diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 2e2cfa684d..0000000000 --- a/.dockerignore +++ /dev/null @@ -1,39 +0,0 @@ -**/.classpath -**/.dockerignore -**/.env -**/.git -**/.gitignore -**/.project -**/.settings -**/.toolstarget -**/.vs -**/.vscode -**/*.*proj.user -**/*.dbmdl -**/*.jfm -**/azds.yaml -**/bin -**/charts -**/docker-compose* -**/Dockerfile* -**/node_modules -**/npm-debug.log -**/obj -**/secrets.dev.yaml -**/values.dev.yaml -**/build -**/docs -**/samples -test -tools -!/tools/uploader-function -*.md -LICENSE - -# SourceLink -!.git/HEAD -!.git/config -!.git/refs/heads - -# Local settings files -local.settings.json diff --git a/.editorconfig b/.editorconfig index b967bb2abc..13485eba02 100644 --- a/.editorconfig +++ b/.editorconfig @@ -20,6 +20,55 @@ generated_code = true [*.cs] charset = utf-8 file_header_template = -------------------------------------------------------------------------------------------------\nCopyright (c) Microsoft Corporation. All rights reserved.\nLicensed under the MIT License (MIT). See LICENSE in the repo root for license information.\n------------------------------------------------------------------------------------------------- +csharp_using_directive_placement = outside_namespace:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_prefer_braces = when_multiline:error +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_prefer_primary_constructors = true:suggestion +csharp_prefer_system_threading_lock = true:suggestion +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:suggestion +csharp_style_expression_bodied_indexers = true:suggestion +csharp_style_expression_bodied_accessors = true:suggestion +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 = false:suggestion +csharp_style_prefer_tuple_swap = true:suggestion +csharp_style_prefer_utf8_string_literals = true:suggestion +csharp_indent_labels = no_change +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable:silent +csharp_prefer_static_local_function = true:suggestion +csharp_prefer_static_anonymous_function = true:suggestion +csharp_style_prefer_readonly_struct = true:suggestion +csharp_style_prefer_readonly_struct_member = true: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_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_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_as_with_null_check = true:suggestion +csharp_style_pattern_matching_over_is_with_cast_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 = false:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +csharp_style_var_elsewhere = false:suggestion # Xml project files [*.{csproj,dcproj}] @@ -54,3 +103,85 @@ end_of_line = lf [*.{cmd,bat}] charset = utf-8 end_of_line = crlf + +[*.{cs,vb}] +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.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_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:error +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:suggestion +dotnet_style_prefer_conditional_expression_over_return = false: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:suggestion +dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion +dotnet_style_namespace_match_folder = true:suggestion +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +end_of_line = crlf +dotnet_style_readonly_field = true:suggestion +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:suggestion +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 +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 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 1a71e97cb6..7a5013dbd4 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -49,8 +49,6 @@ jobs: - name: dotnet build run: | dotnet build Microsoft.Health.Dicom.sln -c Release -p:ContinuousIntegrationBuild=true -warnaserror - dotnet build converter/dicom-cast/Microsoft.Health.DicomCast.sln -c Release -p:ContinuousIntegrationBuild=true -warnaserror - dotnet build tools/Microsoft.Health.Dicom.Tools.sln -c Release -p:ContinuousIntegrationBuild=true -warnaserror if: ${{ matrix.language == 'csharp' }} - name: Perform CodeQL Analysis diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 6e07cda7dd..0000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,32 +0,0 @@ -# Contributing to Medical Imaging Server for DICOM - -This document describes guidelines for contributing to the Medical Imaging Server for DICOM repo. - -## Submitting Pull Requests - -- **DO** submit all changes via pull requests (PRs). They will be reviewed and potentially be merged by maintainers after a peer review that includes at least one of the team members. -- **DO** give PRs short but descriptive names. -- **DO** write a useful but brief description of what the PR is for. -- **DO** refer to any relevant issues and use [keywords](https://help.github.com/articles/closing-issues-using-keywords/) that automatically close issues when the PR is merged. -- **DO** ensure each commit successfully builds. The entire PR must pass all checks before it will be merged. -- **DO** address PR feedback in additional commits instead of amending. -- **DO** assume that [Squash and Merge](https://blog.github.com/2016-04-01-squash-your-commits/) will be used to merge the commits unless specifically requested otherwise. -- **DO NOT** submit "work in progress" PRs. A PR should only be submitted when it is considered ready for review. -- **DO NOT** leave PRs active for more than 4 weeks without a commit. Stale PRs will be closed until they are ready for active development again. -- **DO NOT** mix independent and unrelated changes in one PR. - -## Coding Style - -The coding style is enforced through [.NET analyzers](https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/overview) and an [.editorconfig](.editorconfig) file. Contributors should ensure these guidelines are followed when making submissions. - -- **DO** address the .NET analyzer errors. -- **DO** follow the [.editorconfig](.editorconfig) settings. - -## Creating Issues - -- **DO** use a descriptive title that identifies the issue or the requested feature. -- **DO** write a detailed description of the issue or the requested feature. -- **DO** provide details for issues you create: - - Describe the expected and actual behavior. - - Provide any relevant exception message or OperationOutcome. -- **DO** subscribe to notifications for created issues in case there are any follow-up questions. diff --git a/CredScanSuppressions.json b/CredScanSuppressions.json deleted file mode 100644 index 230b956228..0000000000 --- a/CredScanSuppressions.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "tool": "Credential Scanner", - "suppressions": [ - { - "placeholder": "d8147077-d907-4551-8f40-90c6e86f3f0e", - "_justification": "This is an example value and does not represent a real credential." - }, - { - "placeholder": "globalAdminServicePrincipal", - "_justification": "Service principal for local testing." - }, - { - "placeholder": "123!@#passforCI#$", - "_justification": "Test admin password for testing SQL server using docker during CI" - }, - { - "placeholder": "L0ca1P@ssw0rd", - "_justification": "Test admin password for testing SQL server using docker locally" - }, - { - "placeholder": "T3stP@ssw0rd", - "_justification": "Test admin password for validating the ARM template" - } - ] -} diff --git a/Directory.Packages.props b/Directory.Packages.props index bd36dc020b..5e824f4578 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,118 +1,12 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/GeoPol.xml b/GeoPol.xml index c4ec4057b6..a7de96780b 100644 --- a/GeoPol.xml +++ b/GeoPol.xml @@ -15,7 +15,6 @@ .gitignore - src\Microsoft.Health.Dicom.Web\wwwroot GeoPol.xml diff --git a/GitVersion.yml b/GitVersion.yml deleted file mode 100644 index 10ea6cf34f..0000000000 --- a/GitVersion.yml +++ /dev/null @@ -1,13 +0,0 @@ -mode: Mainline -# assembly-version: If this number changes, other assemblies have to update references to the assembly -assembly-versioning-scheme: Major -assembly-file-versioning-scheme: MajorMinorPatch -ignore: - sha: [] -branches: - main: - is-release-branch: true - feature: - regex: ^(dependabot|dev|feature(s)?|personal|user(s)?)[/-] - hotfix: - tag: useBranchName diff --git a/Microsoft.Health.Dicom.sln b/Microsoft.Health.Dicom.sln index ca7d82f173..430a8a10b5 100644 --- a/Microsoft.Health.Dicom.sln +++ b/Microsoft.Health.Dicom.sln @@ -1,4 +1,3 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31825.309 @@ -10,233 +9,33 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{404C6C33 Directory.Build.props = Directory.Build.props Directory.Packages.props = Directory.Packages.props .config\dotnet-tools.json = .config\dotnet-tools.json - GitVersion.yml = GitVersion.yml global.json = global.json nuget.config = nuget.config EndProjectSection EndProject -Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker\docker-compose.dcproj", "{336B1FB4-EEF8-4E11-BDD5-818983D4E1CD}" - ProjectSection(ProjectDependencies) = postProject - {BFB96311-9B1A-41C1-ABF1-4F6522660084} = {BFB96311-9B1A-41C1-ABF1-4F6522660084} - {C71E1BDD-2B8E-47F3-8801-AE95F5F39941} = {C71E1BDD-2B8E-47F3-8801-AE95F5F39941} - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Dicom.Api", "src\Microsoft.Health.Dicom.Api\Microsoft.Health.Dicom.Api.csproj", "{B0570D75-E376-44AC-870B-87ECB54F0AE3}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Dicom.Api.UnitTests", "src\Microsoft.Health.Dicom.Api.UnitTests\Microsoft.Health.Dicom.Api.UnitTests.csproj", "{D7B538E5-8B3B-487C-8F6A-475F80C50DFE}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Dicom.Blob", "src\Microsoft.Health.Dicom.Blob\Microsoft.Health.Dicom.Blob.csproj", "{1FFFABFB-B30A-4AB8-8193-67016B1C5276}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Dicom.Client", "src\Microsoft.Health.Dicom.Client\Microsoft.Health.Dicom.Client.csproj", "{D100EA60-8DC8-4576-A177-56BC7193BF4A}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Dicom.Core", "src\Microsoft.Health.Dicom.Core\Microsoft.Health.Dicom.Core.csproj", "{E15123F6-5A28-4D86-A28D-30FCB699B9EE}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Dicom.Core.UnitTests", "src\Microsoft.Health.Dicom.Core.UnitTests\Microsoft.Health.Dicom.Core.UnitTests.csproj", "{FA0484A7-AA0C-4CC6-A75F-1D6B23DD847D}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Dicom.Functions", "src\Microsoft.Health.Dicom.Functions\Microsoft.Health.Dicom.Functions.csproj", "{C71E1BDD-2B8E-47F3-8801-AE95F5F39941}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Dicom.Functions.Abstractions", "src\Microsoft.Health.Dicom.Functions.Abstractions\Microsoft.Health.Dicom.Functions.Abstractions.csproj", "{CFAD96C4-1AA3-442D-BADE-A93E4E936742}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Dicom.Functions.Abstractions.UnitTests", "src\Microsoft.Health.Dicom.Functions.Abstractions.UnitTests\Microsoft.Health.Dicom.Functions.Abstractions.UnitTests.csproj", "{2D442B4F-C40A-4102-A332-8048A0655FDC}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Dicom.Functions.App", "src\Microsoft.Health.Dicom.Functions.App\Microsoft.Health.Dicom.Functions.App.csproj", "{BB88616C-6208-43D0-B9EF-79FC0652A151}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Dicom.Functions.Client", "src\Microsoft.Health.Dicom.Functions.Client\Microsoft.Health.Dicom.Functions.Client.csproj", "{F896C916-144F-412E-B3DE-C9D0D9B8EDD1}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Dicom.Functions.Client.UnitTests", "src\Microsoft.Health.Dicom.Functions.Client.UnitTests\Microsoft.Health.Dicom.Functions.Client.UnitTests.csproj", "{3D5D7F69-C766-450A-AA3D-00A50E115E9C}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Dicom.Functions.UnitTests", "src\Microsoft.Health.Dicom.Functions.UnitTests\Microsoft.Health.Dicom.Functions.UnitTests.csproj", "{3D679950-A578-45AD-AF89-FAF89580375F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Dicom.Tests.Common", "src\Microsoft.Health.Dicom.Tests.Common\Microsoft.Health.Dicom.Tests.Common.csproj", "{0F57D85C-8FA4-4DBE-BF44-1CA5109125A5}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Dicom.Tests.Integration", "test\Microsoft.Health.Dicom.Tests.Integration\Microsoft.Health.Dicom.Tests.Integration.csproj", "{DFB41ECC-726C-4DBA-8AD3-17FB0A2546CA}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Dicom.SqlServer", "src\Microsoft.Health.Dicom.SqlServer\Microsoft.Health.Dicom.SqlServer.csproj", "{A88DAB6A-BE0E-41BD-AB48-D1156FB6443C}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Dicom.SqlServer.UnitTests", "src\Microsoft.Health.Dicom.SqlServer.UnitTests\Microsoft.Health.Dicom.SqlServer.UnitTests.csproj", "{ECC018C1-BFA8-44BE-B560-ACB05CA57251}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Dicom.Web", "src\Microsoft.Health.Dicom.Web\Microsoft.Health.Dicom.Web.csproj", "{BFB96311-9B1A-41C1-ABF1-4F6522660084}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Dicom.Web.Tests.E2E", "test\Microsoft.Health.Dicom.Web.Tests.E2E\Microsoft.Health.Dicom.Web.Tests.E2E.csproj", "{1D0ECFDA-2AF2-4796-995D-A7C6E18C9CD1}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{176641B3-297C-4E04-A83D-8F80F80485E8}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{8C9A0050-5D22-4398-9F93-DDCD80B3BA51}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Dicom.Azure", "src\Microsoft.Health.Dicom.Azure\Microsoft.Health.Dicom.Azure.csproj", "{EDF1BF37-3E55-4B6B-B922-C5EEDB71F782}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Dicom.Azure.UnitTests", "src\Microsoft.Health.Dicom.Azure.UnitTests\Microsoft.Health.Dicom.Azure.UnitTests.csproj", "{5571FE42-1EDB-4E25-992A-A37467487DB3}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Dicom.Blob.UnitTests", "src\Microsoft.Health.Dicom.Blob.UnitTests\Microsoft.Health.Dicom.Blob.UnitTests.csproj", "{E2BD0627-CC6C-40D4-B875-00654FD059CF}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Dicom.SchemaManager", "src\Microsoft.Health.Dicom.SchemaManager\Microsoft.Health.Dicom.SchemaManager.csproj", "{5A517D8F-9DBB-4123-99C6-01686BCA4AC7}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Dicom.SchemaManager.UnitTests", "src\Microsoft.Health.Dicom.SchemaManager.UnitTests\Microsoft.Health.Dicom.SchemaManager.UnitTests.csproj", "{C8BB8AF3-DCD1-49A4-A082-9152686AD222}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Dicom.SchemaManager.Console", "src\Microsoft.Health.Dicom.SchemaManager.Console\Microsoft.Health.Dicom.SchemaManager.Console.csproj", "{7AE5FC7B-CE2D-4B5A-B8BB-9B673469EA88}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "forks", "forks", "{AEDC6C96-15FF-4AFB-BD49-3549211AACCF}" - ProjectSection(SolutionItems) = preProject - forks\.globalconfig = forks\.globalconfig - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.FellowOakDicom", "forks\Microsoft.Health.FellowOakDicom\Microsoft.Health.FellowOakDicom.csproj", "{ADD2B971-3C5C-429A-B954-A55FA0FA987D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Health.Dicom.WebUtilities", "src\Microsoft.Health.Dicom.WebUtilities\Microsoft.Health.Dicom.WebUtilities.csproj", "{A454E17D-3A0B-4EC3-85FF-B3CD56B369C8}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 Release|x64 = Release|x64 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {336B1FB4-EEF8-4E11-BDD5-818983D4E1CD}.Debug|x64.ActiveCfg = Debug|Any CPU - {336B1FB4-EEF8-4E11-BDD5-818983D4E1CD}.Debug|x64.Build.0 = Debug|Any CPU - {336B1FB4-EEF8-4E11-BDD5-818983D4E1CD}.Release|x64.ActiveCfg = Release|Any CPU - {336B1FB4-EEF8-4E11-BDD5-818983D4E1CD}.Release|x64.Build.0 = Release|Any CPU - {B0570D75-E376-44AC-870B-87ECB54F0AE3}.Debug|x64.ActiveCfg = Debug|x64 - {B0570D75-E376-44AC-870B-87ECB54F0AE3}.Debug|x64.Build.0 = Debug|x64 - {B0570D75-E376-44AC-870B-87ECB54F0AE3}.Release|x64.ActiveCfg = Release|x64 - {B0570D75-E376-44AC-870B-87ECB54F0AE3}.Release|x64.Build.0 = Release|x64 - {D7B538E5-8B3B-487C-8F6A-475F80C50DFE}.Debug|x64.ActiveCfg = Debug|x64 - {D7B538E5-8B3B-487C-8F6A-475F80C50DFE}.Debug|x64.Build.0 = Debug|x64 - {D7B538E5-8B3B-487C-8F6A-475F80C50DFE}.Release|x64.ActiveCfg = Release|x64 - {D7B538E5-8B3B-487C-8F6A-475F80C50DFE}.Release|x64.Build.0 = Release|x64 - {1FFFABFB-B30A-4AB8-8193-67016B1C5276}.Debug|x64.ActiveCfg = Debug|x64 - {1FFFABFB-B30A-4AB8-8193-67016B1C5276}.Debug|x64.Build.0 = Debug|x64 - {1FFFABFB-B30A-4AB8-8193-67016B1C5276}.Release|x64.ActiveCfg = Release|x64 - {1FFFABFB-B30A-4AB8-8193-67016B1C5276}.Release|x64.Build.0 = Release|x64 {D100EA60-8DC8-4576-A177-56BC7193BF4A}.Debug|x64.ActiveCfg = Debug|x64 {D100EA60-8DC8-4576-A177-56BC7193BF4A}.Debug|x64.Build.0 = Debug|x64 {D100EA60-8DC8-4576-A177-56BC7193BF4A}.Release|x64.ActiveCfg = Release|x64 {D100EA60-8DC8-4576-A177-56BC7193BF4A}.Release|x64.Build.0 = Release|x64 - {E15123F6-5A28-4D86-A28D-30FCB699B9EE}.Debug|x64.ActiveCfg = Debug|x64 - {E15123F6-5A28-4D86-A28D-30FCB699B9EE}.Debug|x64.Build.0 = Debug|x64 - {E15123F6-5A28-4D86-A28D-30FCB699B9EE}.Release|x64.ActiveCfg = Release|x64 - {E15123F6-5A28-4D86-A28D-30FCB699B9EE}.Release|x64.Build.0 = Release|x64 - {FA0484A7-AA0C-4CC6-A75F-1D6B23DD847D}.Debug|x64.ActiveCfg = Debug|x64 - {FA0484A7-AA0C-4CC6-A75F-1D6B23DD847D}.Debug|x64.Build.0 = Debug|x64 - {FA0484A7-AA0C-4CC6-A75F-1D6B23DD847D}.Release|x64.ActiveCfg = Release|x64 - {FA0484A7-AA0C-4CC6-A75F-1D6B23DD847D}.Release|x64.Build.0 = Release|x64 - {C71E1BDD-2B8E-47F3-8801-AE95F5F39941}.Debug|x64.ActiveCfg = Debug|x64 - {C71E1BDD-2B8E-47F3-8801-AE95F5F39941}.Debug|x64.Build.0 = Debug|x64 - {C71E1BDD-2B8E-47F3-8801-AE95F5F39941}.Release|x64.ActiveCfg = Release|x64 - {C71E1BDD-2B8E-47F3-8801-AE95F5F39941}.Release|x64.Build.0 = Release|x64 - {CFAD96C4-1AA3-442D-BADE-A93E4E936742}.Debug|x64.ActiveCfg = Debug|x64 - {CFAD96C4-1AA3-442D-BADE-A93E4E936742}.Debug|x64.Build.0 = Debug|x64 - {CFAD96C4-1AA3-442D-BADE-A93E4E936742}.Release|x64.ActiveCfg = Release|x64 - {CFAD96C4-1AA3-442D-BADE-A93E4E936742}.Release|x64.Build.0 = Release|x64 - {2D442B4F-C40A-4102-A332-8048A0655FDC}.Debug|x64.ActiveCfg = Debug|x64 - {2D442B4F-C40A-4102-A332-8048A0655FDC}.Debug|x64.Build.0 = Debug|x64 - {2D442B4F-C40A-4102-A332-8048A0655FDC}.Release|x64.ActiveCfg = Release|x64 - {2D442B4F-C40A-4102-A332-8048A0655FDC}.Release|x64.Build.0 = Release|x64 - {BB88616C-6208-43D0-B9EF-79FC0652A151}.Debug|x64.ActiveCfg = Debug|x64 - {BB88616C-6208-43D0-B9EF-79FC0652A151}.Debug|x64.Build.0 = Debug|x64 - {BB88616C-6208-43D0-B9EF-79FC0652A151}.Release|x64.ActiveCfg = Release|x64 - {BB88616C-6208-43D0-B9EF-79FC0652A151}.Release|x64.Build.0 = Release|x64 - {F896C916-144F-412E-B3DE-C9D0D9B8EDD1}.Debug|x64.ActiveCfg = Debug|x64 - {F896C916-144F-412E-B3DE-C9D0D9B8EDD1}.Debug|x64.Build.0 = Debug|x64 - {F896C916-144F-412E-B3DE-C9D0D9B8EDD1}.Release|x64.ActiveCfg = Release|x64 - {F896C916-144F-412E-B3DE-C9D0D9B8EDD1}.Release|x64.Build.0 = Release|x64 - {3D5D7F69-C766-450A-AA3D-00A50E115E9C}.Debug|x64.ActiveCfg = Debug|x64 - {3D5D7F69-C766-450A-AA3D-00A50E115E9C}.Debug|x64.Build.0 = Debug|x64 - {3D5D7F69-C766-450A-AA3D-00A50E115E9C}.Release|x64.ActiveCfg = Release|x64 - {3D5D7F69-C766-450A-AA3D-00A50E115E9C}.Release|x64.Build.0 = Release|x64 - {3D679950-A578-45AD-AF89-FAF89580375F}.Debug|x64.ActiveCfg = Debug|x64 - {3D679950-A578-45AD-AF89-FAF89580375F}.Debug|x64.Build.0 = Debug|x64 - {3D679950-A578-45AD-AF89-FAF89580375F}.Release|x64.ActiveCfg = Release|x64 - {3D679950-A578-45AD-AF89-FAF89580375F}.Release|x64.Build.0 = Release|x64 - {0F57D85C-8FA4-4DBE-BF44-1CA5109125A5}.Debug|x64.ActiveCfg = Debug|x64 - {0F57D85C-8FA4-4DBE-BF44-1CA5109125A5}.Debug|x64.Build.0 = Debug|x64 - {0F57D85C-8FA4-4DBE-BF44-1CA5109125A5}.Release|x64.ActiveCfg = Release|x64 - {0F57D85C-8FA4-4DBE-BF44-1CA5109125A5}.Release|x64.Build.0 = Release|x64 - {DFB41ECC-726C-4DBA-8AD3-17FB0A2546CA}.Debug|x64.ActiveCfg = Debug|x64 - {DFB41ECC-726C-4DBA-8AD3-17FB0A2546CA}.Debug|x64.Build.0 = Debug|x64 - {DFB41ECC-726C-4DBA-8AD3-17FB0A2546CA}.Release|x64.ActiveCfg = Release|x64 - {DFB41ECC-726C-4DBA-8AD3-17FB0A2546CA}.Release|x64.Build.0 = Release|x64 - {A88DAB6A-BE0E-41BD-AB48-D1156FB6443C}.Debug|x64.ActiveCfg = Debug|x64 - {A88DAB6A-BE0E-41BD-AB48-D1156FB6443C}.Debug|x64.Build.0 = Debug|x64 - {A88DAB6A-BE0E-41BD-AB48-D1156FB6443C}.Release|x64.ActiveCfg = Release|x64 - {A88DAB6A-BE0E-41BD-AB48-D1156FB6443C}.Release|x64.Build.0 = Release|x64 - {ECC018C1-BFA8-44BE-B560-ACB05CA57251}.Debug|x64.ActiveCfg = Debug|x64 - {ECC018C1-BFA8-44BE-B560-ACB05CA57251}.Debug|x64.Build.0 = Debug|x64 - {ECC018C1-BFA8-44BE-B560-ACB05CA57251}.Release|x64.ActiveCfg = Release|x64 - {ECC018C1-BFA8-44BE-B560-ACB05CA57251}.Release|x64.Build.0 = Release|x64 - {BFB96311-9B1A-41C1-ABF1-4F6522660084}.Debug|x64.ActiveCfg = Debug|x64 - {BFB96311-9B1A-41C1-ABF1-4F6522660084}.Debug|x64.Build.0 = Debug|x64 - {BFB96311-9B1A-41C1-ABF1-4F6522660084}.Release|x64.ActiveCfg = Release|x64 - {BFB96311-9B1A-41C1-ABF1-4F6522660084}.Release|x64.Build.0 = Release|x64 - {1D0ECFDA-2AF2-4796-995D-A7C6E18C9CD1}.Debug|x64.ActiveCfg = Debug|x64 - {1D0ECFDA-2AF2-4796-995D-A7C6E18C9CD1}.Debug|x64.Build.0 = Debug|x64 - {1D0ECFDA-2AF2-4796-995D-A7C6E18C9CD1}.Release|x64.ActiveCfg = Release|x64 - {1D0ECFDA-2AF2-4796-995D-A7C6E18C9CD1}.Release|x64.Build.0 = Release|x64 - {EDF1BF37-3E55-4B6B-B922-C5EEDB71F782}.Debug|x64.ActiveCfg = Debug|x64 - {EDF1BF37-3E55-4B6B-B922-C5EEDB71F782}.Debug|x64.Build.0 = Debug|x64 - {EDF1BF37-3E55-4B6B-B922-C5EEDB71F782}.Release|x64.ActiveCfg = Release|x64 - {EDF1BF37-3E55-4B6B-B922-C5EEDB71F782}.Release|x64.Build.0 = Release|x64 - {5571FE42-1EDB-4E25-992A-A37467487DB3}.Debug|x64.ActiveCfg = Debug|x64 - {5571FE42-1EDB-4E25-992A-A37467487DB3}.Debug|x64.Build.0 = Debug|x64 - {5571FE42-1EDB-4E25-992A-A37467487DB3}.Release|x64.ActiveCfg = Release|x64 - {5571FE42-1EDB-4E25-992A-A37467487DB3}.Release|x64.Build.0 = Release|x64 - {E2BD0627-CC6C-40D4-B875-00654FD059CF}.Debug|x64.ActiveCfg = Debug|x64 - {E2BD0627-CC6C-40D4-B875-00654FD059CF}.Debug|x64.Build.0 = Debug|x64 - {E2BD0627-CC6C-40D4-B875-00654FD059CF}.Release|x64.ActiveCfg = Release|x64 - {E2BD0627-CC6C-40D4-B875-00654FD059CF}.Release|x64.Build.0 = Release|x64 - {5A517D8F-9DBB-4123-99C6-01686BCA4AC7}.Debug|x64.ActiveCfg = Debug|x64 - {5A517D8F-9DBB-4123-99C6-01686BCA4AC7}.Debug|x64.Build.0 = Debug|x64 - {5A517D8F-9DBB-4123-99C6-01686BCA4AC7}.Release|x64.ActiveCfg = Release|x64 - {5A517D8F-9DBB-4123-99C6-01686BCA4AC7}.Release|x64.Build.0 = Release|x64 - {C8BB8AF3-DCD1-49A4-A082-9152686AD222}.Debug|x64.ActiveCfg = Debug|x64 - {C8BB8AF3-DCD1-49A4-A082-9152686AD222}.Debug|x64.Build.0 = Debug|x64 - {C8BB8AF3-DCD1-49A4-A082-9152686AD222}.Release|x64.ActiveCfg = Release|x64 - {C8BB8AF3-DCD1-49A4-A082-9152686AD222}.Release|x64.Build.0 = Release|x64 - {7AE5FC7B-CE2D-4B5A-B8BB-9B673469EA88}.Debug|x64.ActiveCfg = Debug|x64 - {7AE5FC7B-CE2D-4B5A-B8BB-9B673469EA88}.Debug|x64.Build.0 = Debug|x64 - {7AE5FC7B-CE2D-4B5A-B8BB-9B673469EA88}.Release|x64.ActiveCfg = Release|x64 - {7AE5FC7B-CE2D-4B5A-B8BB-9B673469EA88}.Release|x64.Build.0 = Release|x64 - {ADD2B971-3C5C-429A-B954-A55FA0FA987D}.Debug|x64.ActiveCfg = Debug|x64 - {ADD2B971-3C5C-429A-B954-A55FA0FA987D}.Debug|x64.Build.0 = Debug|x64 - {ADD2B971-3C5C-429A-B954-A55FA0FA987D}.Release|x64.ActiveCfg = Release|x64 - {ADD2B971-3C5C-429A-B954-A55FA0FA987D}.Release|x64.Build.0 = Release|x64 - {A454E17D-3A0B-4EC3-85FF-B3CD56B369C8}.Debug|x64.ActiveCfg = Debug|x64 - {A454E17D-3A0B-4EC3-85FF-B3CD56B369C8}.Debug|x64.Build.0 = Debug|x64 - {A454E17D-3A0B-4EC3-85FF-B3CD56B369C8}.Release|x64.ActiveCfg = Release|x64 - {A454E17D-3A0B-4EC3-85FF-B3CD56B369C8}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {B0570D75-E376-44AC-870B-87ECB54F0AE3} = {176641B3-297C-4E04-A83D-8F80F80485E8} - {D7B538E5-8B3B-487C-8F6A-475F80C50DFE} = {176641B3-297C-4E04-A83D-8F80F80485E8} - {1FFFABFB-B30A-4AB8-8193-67016B1C5276} = {176641B3-297C-4E04-A83D-8F80F80485E8} {D100EA60-8DC8-4576-A177-56BC7193BF4A} = {176641B3-297C-4E04-A83D-8F80F80485E8} - {E15123F6-5A28-4D86-A28D-30FCB699B9EE} = {176641B3-297C-4E04-A83D-8F80F80485E8} - {FA0484A7-AA0C-4CC6-A75F-1D6B23DD847D} = {176641B3-297C-4E04-A83D-8F80F80485E8} - {C71E1BDD-2B8E-47F3-8801-AE95F5F39941} = {176641B3-297C-4E04-A83D-8F80F80485E8} - {CFAD96C4-1AA3-442D-BADE-A93E4E936742} = {176641B3-297C-4E04-A83D-8F80F80485E8} - {2D442B4F-C40A-4102-A332-8048A0655FDC} = {176641B3-297C-4E04-A83D-8F80F80485E8} - {BB88616C-6208-43D0-B9EF-79FC0652A151} = {176641B3-297C-4E04-A83D-8F80F80485E8} - {F896C916-144F-412E-B3DE-C9D0D9B8EDD1} = {176641B3-297C-4E04-A83D-8F80F80485E8} - {3D5D7F69-C766-450A-AA3D-00A50E115E9C} = {176641B3-297C-4E04-A83D-8F80F80485E8} - {3D679950-A578-45AD-AF89-FAF89580375F} = {176641B3-297C-4E04-A83D-8F80F80485E8} - {0F57D85C-8FA4-4DBE-BF44-1CA5109125A5} = {176641B3-297C-4E04-A83D-8F80F80485E8} - {DFB41ECC-726C-4DBA-8AD3-17FB0A2546CA} = {8C9A0050-5D22-4398-9F93-DDCD80B3BA51} - {A88DAB6A-BE0E-41BD-AB48-D1156FB6443C} = {176641B3-297C-4E04-A83D-8F80F80485E8} - {ECC018C1-BFA8-44BE-B560-ACB05CA57251} = {176641B3-297C-4E04-A83D-8F80F80485E8} - {BFB96311-9B1A-41C1-ABF1-4F6522660084} = {176641B3-297C-4E04-A83D-8F80F80485E8} - {1D0ECFDA-2AF2-4796-995D-A7C6E18C9CD1} = {8C9A0050-5D22-4398-9F93-DDCD80B3BA51} - {EDF1BF37-3E55-4B6B-B922-C5EEDB71F782} = {176641B3-297C-4E04-A83D-8F80F80485E8} - {5571FE42-1EDB-4E25-992A-A37467487DB3} = {176641B3-297C-4E04-A83D-8F80F80485E8} - {E2BD0627-CC6C-40D4-B875-00654FD059CF} = {176641B3-297C-4E04-A83D-8F80F80485E8} - {5A517D8F-9DBB-4123-99C6-01686BCA4AC7} = {176641B3-297C-4E04-A83D-8F80F80485E8} - {C8BB8AF3-DCD1-49A4-A082-9152686AD222} = {176641B3-297C-4E04-A83D-8F80F80485E8} - {7AE5FC7B-CE2D-4B5A-B8BB-9B673469EA88} = {176641B3-297C-4E04-A83D-8F80F80485E8} - {ADD2B971-3C5C-429A-B954-A55FA0FA987D} = {AEDC6C96-15FF-4AFB-BD49-3549211AACCF} - {A454E17D-3A0B-4EC3-85FF-B3CD56B369C8} = {176641B3-297C-4E04-A83D-8F80F80485E8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {E370FB31-CF95-47D1-B1E1-863A77973FF8} RESX_SortFileContentOnSave = True + SolutionGuid = {E370FB31-CF95-47D1-B1E1-863A77973FF8} EndGlobalSection EndGlobal diff --git a/README.md b/README.md index 7504ac34e6..e2411a9959 100644 --- a/README.md +++ b/README.md @@ -1,91 +1,32 @@ # Medical Imaging Server for DICOM > [!IMPORTANT] -> 📢 **After more than 3 years, our team is planning to archive the DICOM server project to allow us to focus on delivering customer value to our [managed service offering on Azure](https://learn.microsoft.com/en-us/azure/healthcare-apis/dicom/overview). We plan to archive the DICOM server project on April 30, 2024** +> 📢 **After more than 3 years, our team is planning to archive the DICOM server project to allow us to focus on delivering customer value to our [managed service offering on Azure](https://learn.microsoft.com/en-us/azure/healthcare-apis/dicom/overview).** > > Learn more in our recent [discussion post](https://github.com/microsoft/dicom-server/discussions/3401) -The Medical Imaging Server for DICOM is an open source DICOM server that is easily deployed on Azure. It allows standards-based communication with any DICOMweb™ enabled systems, and injects DICOM metadata into a FHIR server to create a holistic view of patient data. The Medical Imaging Server for DICOM integrates tightly with the [FHIR Server for Azure](https://github.com/microsoft/fhir-server) enabling healthcare professionals, ISVs, and medical device vendors to create new and innovative solutions. FHIR is becoming an important standard for clinical data and provides extensibility to support integration of other types of data directly, or through references. By using the Medical Imaging Server for DICOM, organizations can store references to imaging data in FHIR and enable queries across clinical and imaging datasets. +## Project Cleanup: Client Code Retention -![Architecture](docs/images/DICOM-arch.png) +This repository has undergone a significant cleanup. All code except for the client has been removed. If you need access to the removed code, it has been preserved in an archived branch for reference. -The Medical Imaging Server for DICOM is a .NET Core implementation of DICOMweb™. [DICOMweb™](https://www.dicomstandard.org/using/dicomweb) is the DICOM Standard for web-based medical imaging. Details of our conformance to the standard can be found in our [Conformance Statement](docs/resources/conformance-statement.md). +### Key Details -## Managed service +**Current State**: Only the client code remains in the main branch. -Azure Health Data Service [DICOM service](https://docs.microsoft.com/en-us/azure/healthcare-apis/dicom/deploy-dicom-services-in-azure) is a managed service and recommended for production deployment. +**Archived Code**: All other components have been moved to the [`archived`](https://github.com/microsoft/dicom-server/tree/archived) branch. -Only the Nuget libraries released from this repo are meant to be used in production. -Review [maintainance guide]( ./docs/resources/dicom-server-maintaince-guide.md) if you want to manage your own deployment from this repo. +### Accessing the Archived Code +To access the archived code: -## Deploy the Medical Imaging Server for DICOM +1. Clone the repository if you haven't already: -The Medical Imaging Server for DICOM is designed to run on Azure. However, for development and test environments it can be deployed locally as a set of Docker containers to speed up development. + `git clone https://github.com/microsoft/dicom-server.git` -### Deploy to Azure +2. Switch to the archive branch: -If you already have an Azure subscription, deploy the Medical Imaging Server for DICOM directly to Azure:
- + `git checkout archived` -To sync your Medical Imaging Server for DICOM metadata directly into a FHIR server, deploy **DICOM Cast** (alongside a FHIR OSS Server and Medical Imaging Server for DICOM) via:
- - - -For a complete set of instructions for how to deploy the Medical Imaging Server for DICOM to Azure, refer to the Quickstart [Deploy to Azure ](docs/quickstarts/deploy-via-azure.md). - -### Deploy locally - -Follow the [Development Setup Instructions](docs/development/setup.md) to deploy a local copy of the Medical Imaging Server for DICOM. Be aware that this deployment leverages the [Azurite container](https://github.com/Azure/Azurite) which emulates the Azure Storage API, and should not be used in production. - -Note that the webapp library, ARM templates and Web.Zip package are for testing purposes only, they are not recommended for production scenarios. These will not be versioned. You can find the artifact feed generated by the Medical Imaging Server for DICOM at the [Azure Devops Public Feed](https://microsofthealthoss.visualstudio.com/FhirServer/_packaging?_a=feed&feed=Public), including the versioned packages. - -## Quickstarts - -- [Deploy Medical Imaging Server for DICOM via Azure](docs/quickstarts/deploy-via-azure.md) -- [Deploy Medical Imaging Server for DICOM via Docker](docs/quickstarts/deploy-via-docker.md) -- [Set up DICOM Cast](docs/quickstarts/deploy-dicom-cast.md) - -## Tutorials - -- [Use the Medical Imaging Server for DICOM APIs](docs/tutorials/use-the-medical-imaging-server-apis.md) -- [Use DICOMweb™ Standard APIs with C#](docs/tutorials/use-dicom-web-standard-apis-with-c%23.md) -- [Use DICOMweb™ Standard APIs with Python](docs/tutorials/use-dicom-web-standard-apis-with-python.md) -- [Use DICOMweb™ Standard APIs with cURL](docs/tutorials/use-dicom-web-standard-apis-with-curl.md) - -## How-to guides - -- [Configure Medical Imaging Server for DICOM server settings](docs/how-to-guides/configure-dicom-server-settings.md) -- [Enable Authentication and retrieve an OAuth token](docs/how-to-guides/enable-authentication-with-tokens.md) -- [Enable Authorization](docs/how-to-guides/enable-authorization.md) -- [Pull Changes from Medical Imaging Server for DICOM with Change Feed](docs/how-to-guides/pull-changes-from-change-feed.md) -- [Sync DICOM metadata to FHIR](docs/how-to-guides/sync-dicom-metadata-to-fhir.md) -- [Extended Query Tags](docs/how-to-guides/extended-query-tags.md) - -## Concepts - -- [DICOM](docs/concepts/dicom.md) -- [Change Feed](docs/concepts/change-feed.md) -- [DICOM Cast](docs/concepts/dicom-cast.md) - -## Resources - -- [FAQ](docs/resources/faq.md) -- [Conformance Statement](docs/resources/conformance-statement.md) -- [Health Check API](docs/resources/health-check-api.md) -- [Performance Guidance](docs/resources/performance-guidance.md) -- [Api Versions](docs/api-versioning.md) - -## Development - -- [Setup](docs/development/setup.md) -- [Code Organization](docs/development/code-organization.md) -- [Naming Guidelines](docs/development/naming-guidelines.md) -- [Exception handling](docs/development/exception-handling.md) -- [Tests](docs/development/tests.md) -- [Identity Server Authentication](docs/development/identity-server-authentication.md) -- [Roles](docs/development/roles.md) -- [API Versioning (developer)](docs/development/api-versioning-developers.md) ## Contributing @@ -98,14 +39,8 @@ a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow th provided by the bot. You will only need to do this once across all repositories using our CLA. There are many other ways to contribute to Medical Imaging Server for DICOM. -* [Submit bugs](https://github.com/Microsoft/dicom-server/issues) and help us verify fixes as they are checked in. -* Review the [source code changes](https://github.com/Microsoft/dicom-server/pulls). -* Engage with Medical Imaging Server for DICOM users and developers on [StackOverflow](https://stackoverflow.com/questions/tagged/medical-imaging-server-for-dicom). * Join the [#dicomonazure](https://twitter.com/hashtag/dicomonazure?f=tweets&vertical=default) discussion on Twitter. -* [Contribute bug fixes](CONTRIBUTING.md). - -See [Contributing to Medical Imaging Server for DICOM](CONTRIBUTING.md) for more information. This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or -contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. +contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. \ No newline at end of file diff --git a/build/.vsts-ci.yml b/build/.vsts-ci.yml deleted file mode 100644 index 18c79baf6b..0000000000 --- a/build/.vsts-ci.yml +++ /dev/null @@ -1,150 +0,0 @@ -# DESCRIPTION: -# Builds, tests and packages the solution for the CI build configuration. -name: $(SourceBranchName)-$(Date:yyyyMMdd)$(Rev:-r) - -variables: -- template: ci/variables.yml - -parameters: -- name: push - displayName: Push - type: boolean - default: true - -trigger: - branches: - include: - - main - paths: - include: - - '*' - exclude: - - '*.md' - - docs - -pr: none - -stages: -- stage: UpdateVersion - displayName: 'Determine Semantic Version' - jobs: - - job: GitVersion - pool: - vmImage: 'ubuntu-latest' - steps: - - template: ./common/update-semver.yml - -- stage: BuildElectron - displayName: 'Build Electron Tool' - jobs: - - job: NodeJs - pool: - vmImage: 'ubuntu-latest' - steps: - - template: common/build-electron.yml - -- stage: BuildDotNet - displayName: 'Build and Run Unit Tests' - dependsOn: - - UpdateVersion - variables: - assemblySemVer: $[stageDependencies.UpdateVersion.GitVersion.outputs['DicomVersion.GitVersion.AssemblySemVer']] - assemblySemFileVer: $[stageDependencies.UpdateVersion.GitVersion.outputs['DicomVersion.GitVersion.AssemblySemFileVer']] - informationalVersion: $[stageDependencies.UpdateVersion.GitVersion.outputs['DicomVersion.GitVersion.InformationalVersion']] - majorMinorPatch: $[stageDependencies.UpdateVersion.GitVersion.outputs['DicomVersion.GitVersion.MajorMinorPatch']] - nuGetVersion: $[stageDependencies.UpdateVersion.GitVersion.outputs['DicomVersion.GitVersion.SemVer']] - jobs: - - job: DotNet - displayName: 'Build DICOM Projects' - pool: - vmImage: 'ubuntu-latest' - steps: - - template: common/build-dotnet.yml - -- stage: AnalyzeSecurity - displayName: 'Run Security Analysis' - dependsOn: - - BuildDotNet - jobs: - - job: Guardian - pool: - vmImage: 'windows-latest' - steps: - - template: common/analyze.yml - -- stage: DeployTestEnvironment - displayName: 'Deploy Test Environment' - dependsOn: - - BuildDotNet - jobs: - - template: ci/deploy.yml - -- stage: ValidateAPIVersioning - displayName: 'Detect Breaking Changes In API' - dependsOn: - - BuildDotNet - jobs: - - template: common/versioning.yml - -- stage: DeployFeaturesEnabledWebapp - displayName: 'Deploy features-enabled webapp' - dependsOn: - - DeployTestEnvironment - jobs: - - template: ci/deploy-features-enabled-webapp.yml - -- stage: RunIntegrationTests - displayName: 'Run Integration Tests' - dependsOn: - - DeployTestEnvironment - jobs: - - template: ci/run-integration-tests.yml - -- stage: RunE2ETests - displayName: 'Run E2E Tests' - dependsOn: - - DeployTestEnvironment - jobs: - - template: ci/run-e2e-tests.yml - -- stage: RunE2EFeaturesEnabledTests - displayName: 'Run E2E features-enabled tests' - dependsOn: - - DeployFeaturesEnabledWebapp - jobs: - - template: ci/run-e2e-features-enabled-tests.yml - -- stage: PublishNuget - displayName: 'Publish NuGet Packages' - condition: eq(${{ parameters.push }}, true) - dependsOn: - - AnalyzeSecurity - - ValidateAPIVersioning - - RunIntegrationTests - - RunE2ETests - - RunE2EFeaturesEnabledTests - jobs: - - job: PublishNugets - pool: - vmImage: 'windows-latest' - steps: - - template: ci/publish-nuget.yml - -- stage: PublishContainer - displayName: 'Publish Docker CI Container' - dependsOn: - - AnalyzeSecurity - - ValidateAPIVersioning - - RunIntegrationTests - - RunE2ETests - - RunE2EFeaturesEnabledTests - jobs: - - job: 'Docker' - displayName: 'Build and Push Docker Images' - pool: - vmImage: 'ubuntu-latest' - steps: - - template: common/docker-build-push.yml - parameters: - tag: $(imageTag) - push: ${{ parameters.push }} diff --git a/build/.vsts-pr.arm.yml b/build/.vsts-pr.arm.yml deleted file mode 100644 index 459c3c652e..0000000000 --- a/build/.vsts-pr.arm.yml +++ /dev/null @@ -1,43 +0,0 @@ -# DESCRIPTION: -# Validates the ARM templates in the event of changes - -pr: - branches: - include: - - main - paths: - include: - - build - - samples/templates - -trigger: none - -variables: -- template: pr/variables.yml - -# Note: ARMory currently only works on Windows -pool: - vmImage: 'windows-latest' - -steps: - - task: AzureResourceManagerTemplateDeployment@3 - displayName: 'Validate ARM Template' - inputs: - deploymentScope: 'Resource Group' - azureResourceManagerConnection: '$(azureSubscriptionName)' - subscriptionId: '$(azureSubscriptionId)' - action: 'Create Or Update Resource Group' - resourceGroupName: '$(appServicePlanResourceGroup)' - location: 'West US 2' - templateLocation: 'Linked artifact' - csmFile: '$(Build.Repository.LocalPath)/samples/templates/default-azuredeploy.json' - overrideParameters: '-serviceName "$(deploymentName)" -location "$(resourceGroupRegion)" -sqlAdminPassword "T3stP@ssw0rd"' - deploymentMode: 'Validation' - deploymentName: 'ValidateDicom$(System.PullRequest.PullRequestNumber)' - - - template: common/analyze.yml - parameters: - analyzeBinaries: false - analyzePackages: false - runAntiMalware: false - credScanDirectory: '$(Build.Repository.LocalPath)/samples/templates' diff --git a/build/.vsts-pr.dotnet.yml b/build/.vsts-pr.dotnet.yml deleted file mode 100644 index 83f5c2e136..0000000000 --- a/build/.vsts-pr.dotnet.yml +++ /dev/null @@ -1,112 +0,0 @@ -# DESCRIPTION: -# Builds, tests and packages the .NET solutions - -pr: - branches: - include: - - main - paths: - include: - - '*' - exclude: - - '*.md' - - docs - - samples/templates - - tools/dicom-web-electron - -trigger: none - -variables: -- template: pr/variables.yml - -stages: -- stage: UpdateVersion - displayName: 'Determine Semantic Version' - jobs: - - job: GitVersion - pool: - vmImage: 'ubuntu-latest' - steps: - - template: ./common/update-semver.yml - - powershell: | - $buildNumber = "$(GitVersion.SemVer)" -replace "\.", "" - Write-Host "##vso[build.updatebuildnumber]$buildNumber" - Write-Host "Updated build number to '$buildNumber'" - name: SetBuildVersion - -- stage: BuildDotNet - displayName: 'Build and Run Unit Tests' - dependsOn: - - UpdateVersion - variables: - assemblySemVer: $[stageDependencies.UpdateVersion.GitVersion.outputs['DicomVersion.GitVersion.AssemblySemVer']] - assemblySemFileVer: $[stageDependencies.UpdateVersion.GitVersion.outputs['DicomVersion.GitVersion.AssemblySemFileVer']] - informationalVersion: $[stageDependencies.UpdateVersion.GitVersion.outputs['DicomVersion.GitVersion.InformationalVersion']] - majorMinorPatch: $[stageDependencies.UpdateVersion.GitVersion.outputs['DicomVersion.GitVersion.MajorMinorPatch']] - nuGetVersion: $[stageDependencies.UpdateVersion.GitVersion.outputs['DicomVersion.GitVersion.SemVer']] - jobs: - - job: DotNet - displayName: 'Build DICOM Projects' - pool: - vmImage: 'ubuntu-latest' - steps: - - template: common/build-dotnet.yml - parameters: - packageArtifacts: true - packageNugets: false - -- stage: DockerBuild - displayName: 'Build Docker' - dependsOn: - - BuildDotNet - jobs: - - job: Docker - displayName: docker build - pool: - vmImage: 'ubuntu-latest' - steps: - - template: common/docker-build-push.yml - parameters: - tag: $(imageTag) - push: false - -- stage: AnalyzeSecurity - displayName: 'Run Security Analysis' - dependsOn: - - BuildDotNet - jobs: - - job: Guardian - pool: - vmImage: 'windows-latest' - steps: - - template: common/analyze.yml - parameters: - analyzePackages: false - -- stage: ValidateAPIVersioning - displayName: 'Detect Breaking REST API Changes' - dependsOn: - - BuildDotNet - jobs: - - template: common/versioning.yml - -- stage: RunIntegrationTests - displayName: 'Run Integration tests' - dependsOn: - - BuildDotNet - jobs: - - template: pr/run-integration-tests.yml - -- stage: RunE2ETests - displayName: 'Run E2E tests' - dependsOn: - - BuildDotNet - jobs: - - template: pr/run-e2e-tests.yml - -- stage: RunE2EFeaturesEnabledTests - displayName: 'Run E2E features-enabled tests' - dependsOn: - - BuildDotNet - jobs: - - template: pr/run-e2e-features-enabled-tests.yml diff --git a/build/.vsts-pr.node.yml b/build/.vsts-pr.node.yml deleted file mode 100644 index 88f171bb13..0000000000 --- a/build/.vsts-pr.node.yml +++ /dev/null @@ -1,34 +0,0 @@ -# DESCRIPTION: -# Builds the JavaScript tools - -pr: - branches: - include: - - main - paths: - include: - - build - - tools/dicom-web-electron - -variables: -- template: pr/variables.yml - -trigger: none - -pool: - vmImage: 'ubuntu-latest' - -steps: - - template: common/build-electron.yml - # Task is needed to bypass Guardian issue until Guardian team has fixed their script to get sdk for linux platforms - - task: UseDotNet@2 - displayName: 'Use .NET Core sdk' - inputs: - version: '3.1.201' - - template: common/analyze.yml - parameters: - analyzeARMTemplates: false - analyzeBinaries: false - analyzePackages: false - runAntiMalware: false - credScanDirectory: '$(Build.Repository.LocalPath)/tools/dicom-web-electron' diff --git a/build/ci/add-aad-test-environment.yml b/build/ci/add-aad-test-environment.yml deleted file mode 100644 index 8567f1c626..0000000000 --- a/build/ci/add-aad-test-environment.yml +++ /dev/null @@ -1,58 +0,0 @@ -steps: - -- task: AzureKeyVault@1 - displayName: 'Azure Key Vault: resolute-oss-tenant-info' - inputs: - azureSubscription: $(azureSubscriptionName) - KeyVaultName: 'resolute-oss-tenant-info' - -- task: AzurePowerShell@5 - displayName: Setup Aad Test Tenant - inputs: - azureSubscription: $(azureSubscriptionName) - azurePowerShellVersion: latestVersion - ScriptType: inlineScript - Inline: | - Install-Module -Name AzureAD -Repository PSGallery -Scope CurrentUser -AcceptLicense -Force - Import-Module -Name AzureAD - - $tenantId = "$(tenant-id)" - - # Get admin token - $username = "$(tenant-admin-user-name)" - $password_raw = - @" - $(tenant-admin-user-password) - "@ - $password = ConvertTo-SecureString -AsPlainText $password_raw -Force - $adminCredential = New-Object PSCredential $username,$password - - $adTokenUrl = "https://login.microsoftonline.com/$tenantId/oauth2/token" - $resource = "https://graph.windows.net/" - - $body = @{ - grant_type = "password" - username = $username - password = $password_raw - resource = $resource - client_id = "1950a258-227b-4e31-a9cf-717495945fc2" # Microsoft Azure PowerShell - } - - # If a deleted keyvault exists, remove it - if (Get-AzKeyVault -VaultName "$(deploymentName)-ts" -Location "$(resourceGroupRegion)" -InRemovedState) - { - Write-Host "A deleted keyvault '$(deploymentName)-ts' found in '$(resourceGroupRegion)'. Attempting to remove." - Remove-AzKeyVault -VaultName "$(deploymentName)-ts" -InRemovedState -Location "$(resourceGroupRegion)" -Force - } - else - { - Write-Host "A deleted keyvault '$(deploymentName)-ts' not found in '$(resourceGroupRegion)' - this is normal" - } - - $response = Invoke-RestMethod -Method 'Post' -Uri $adTokenUrl -ContentType "application/x-www-form-urlencoded" -Body $body - Connect-AzureAD -TenantId $tenantId -AadAccessToken $response.access_token -AccountId $username - - Import-Module $(System.DefaultWorkingDirectory)/samples/scripts/PowerShell/DicomServer.psd1 - Import-Module $(System.DefaultWorkingDirectory)/release/scripts/PowerShell/DicomServerRelease/DicomServerRelease.psd1 - - $output = Add-AadTestAuthEnvironment -TestAuthEnvironmentPath $(System.DefaultWorkingDirectory)/testauthenvironment.json -EnvironmentName $(deploymentName) -TenantAdminCredential $adminCredential -EnvironmentLocation $(resourceGroupRegion) -TenantIdDomain $tenantId diff --git a/build/ci/deploy-features-enabled-webapp.yml b/build/ci/deploy-features-enabled-webapp.yml deleted file mode 100644 index 5b06bb261e..0000000000 --- a/build/ci/deploy-features-enabled-webapp.yml +++ /dev/null @@ -1,61 +0,0 @@ -jobs: -- job: provision - displayName: 'Provision DICOM' - pool: - vmImage: 'windows-latest' - variables: - - name: sqlAdminPassword - value: $[ stageDependencies.DeployTestEnvironment.provision.outputs['deploy.sqlAdminPassword'] ] - steps: - - - task: AzureKeyVault@1 - displayName: 'Azure Key Vault: resolute-oss-tenant-info' - inputs: - azureSubscription: $(azureSubscriptionName) - KeyVaultName: 'resolute-oss-tenant-info' - - - task: AzurePowerShell@5 - displayName: 'Features-enabled webapp infrastructure deployment' - inputs: - azureSubscription: $(azureSubscriptionName) - azurePowerShellVersion: latestVersion - ScriptType: InlineScript - Inline: | - Add-Type -AssemblyName System.Web - - $deployPath = "$(System.DefaultWorkingDirectory)/release/templates" - - $additionalProperties = @{ - "DicomServer__Features__EnableDataPartitions" = "true" - "DicomServer__Features__EnableLatestApiVersion" = "true" - } - - $templateParameters = @{ - serviceName = "$(deploymentName)" - appServicePlanResourceGroup = "$(appServicePlanResourceGroup)" - appServicePlanName = "$(appServicePlanName)" - additionalDicomServerConfigProperties = $additionalProperties - sqlAdminPassword = "$(sqlAdminPassword)" - securityAuthenticationAuthority = "https://login.microsoftonline.com/$(tenant-id)" - securityAuthenticationAudience = "$(testApplicationResource)" - } - - New-AzResourceGroupDeployment -Name "$(deploymentName)-features-enabled-webapp" -ResourceGroupName "$(resourceGroupName)" -TemplateFile $deployPath/featuresenabled-azuredeploy.json -TemplateParameterObject $templateParameters -Verbose - - - task: DownloadBuildArtifacts@0 - inputs: - buildType: 'current' - downloadType: 'single' - downloadPath: '$(System.ArtifactsDirectory)' - artifactName: 'deploy' - - - task: AzureRMWebAppDeployment@4 - displayName: 'Features-enabled webapp package deployment' - inputs: - connectionType: 'AzureRM' - azureSubscription: $(azureSubscriptionName) - webAppKind: 'webApp' - webAppName: '$(deploymentName)-featuresenabled' - package: '$(System.ArtifactsDirectory)/deploy/Microsoft.Health.Dicom.Web.zip' - takeAppOfflineFlag: true - deploymentType: zipDeploy diff --git a/build/ci/deploy.yml b/build/ci/deploy.yml deleted file mode 100644 index 90853bb6dd..0000000000 --- a/build/ci/deploy.yml +++ /dev/null @@ -1,85 +0,0 @@ -jobs: -- job: Provision - displayName: 'Provision DICOM' - pool: - vmImage: 'windows-latest' - steps: - - task: AzurePowerShell@5 - displayName: 'New Resource Group' - inputs: - azureSubscription: $(azureSubscriptionName) - azurePowerShellVersion: latestVersion - ScriptType: InlineScript - Inline: | - New-AzResourceGroup -Name "$(resourceGroupName)" -Location "$(resourceGroupRegion)" -Force - - - template: add-aad-test-environment.yml - - - task: AzurePowerShell@5 - name: deploy - displayName: 'New Azure resource group deployment' - inputs: - azureSubscription: $(azureSubscriptionName) - azurePowerShellVersion: latestVersion - ScriptType: InlineScript - Inline: | - Add-Type -AssemblyName System.Web - - $deployPath = "$(System.DefaultWorkingDirectory)/samples/templates" - - $additionalProperties = @{ - "SqlServer__DeleteAllDataOnStartup" = "$(deleteDataOnStartup)" - "DicomServer__Security__Authorization__Enabled" = "true" - } - - $sqlAdminPassword = "$(-join((((33,35,37,38,42,43,45,46,95) + (48..57) + (65..90) + (97..122) | Get-Random -Count 20) + ((33,35,37,38,42,43,45,46,95) | Get-Random -Count 1) + ((48..57) | Get-Random -Count 1) + ((65..90) | Get-Random -Count 1) + ((97..122) | Get-Random -Count 1) | Get-Random -Count 24) | % {[char]$_}))" - Write-Host "##vso[task.setvariable variable=sqlAdminPassword;isSecret=true;isOutput=true]$sqlAdminPassword" - - $templateParameters = @{ - serviceName = "$(deploymentName)" - functionAppName = "$(deploymentName)-functions" - appServicePlanResourceGroup = "$(appServicePlanResourceGroup)" - appServicePlanName = "$(appServicePlanName)" - additionalDicomServerConfigProperties = $additionalProperties - sqlAdminPassword = $sqlAdminPassword - securityAuthenticationAuthority = "https://login.microsoftonline.com/$(tenant-id)" - securityAuthenticationAudience = "$(testApplicationResource)" - deployPackage = $false - } - - $deployment = New-AzResourceGroupDeployment -Name "$(deploymentName)" -ResourceGroupName "$(resourceGroupName)" -TemplateFile $deployPath/default-azuredeploy.json -TemplateParameterObject $templateParameters -Verbose - - Set-AzKeyVaultAccessPolicy -VaultName "$(deploymentName)" -ObjectId $(azureServiceConnectionOid) -PermissionsToSecrets list,get -BypassObjectIdValidation - - $storageAccountName = $deployment.Outputs['storageAccountName'].Value - Write-Host "##vso[task.setvariable variable=azureStorageAccountName;isOutput=true]$storageAccountName" - - - task: DownloadBuildArtifacts@0 - displayName: 'Download Deployment Binaries' - inputs: - buildType: 'current' - downloadType: 'single' - downloadPath: '$(System.ArtifactsDirectory)' - artifactName: 'deploy' - - - task: AzureRMWebAppDeployment@4 - displayName: 'Deploy Dicom Web App' - inputs: - connectionType: 'AzureRM' - azureSubscription: $(azureSubscriptionName) - webAppKind: 'webApp' - webAppName: '$(deploymentName)' - package: '$(System.ArtifactsDirectory)/deploy/Microsoft.Health.Dicom.Web.zip' - takeAppOfflineFlag: true - deploymentType: zipDeploy - - - task: AzureRMWebAppDeployment@4 - displayName: 'Deploy Dicom Functions' - inputs: - connectionType: 'AzureRM' - azureSubscription: $(azureSubscriptionName) - webAppKind: 'functionApp' - webAppName: '$(deploymentName)-functions' - package: '$(System.ArtifactsDirectory)/deploy/Microsoft.Health.Dicom.Functions.App.zip' - takeAppOfflineFlag: true - deploymentType: zipDeploy diff --git a/build/ci/publish-nuget.yml b/build/ci/publish-nuget.yml deleted file mode 100644 index b3beda14df..0000000000 --- a/build/ci/publish-nuget.yml +++ /dev/null @@ -1,37 +0,0 @@ -steps: - - task: UseDotNet@2 - displayName: 'Use .NET Core sdk' - inputs: - useGlobalJson: true - - - task: DownloadBuildArtifacts@0 - inputs: - buildType: 'current' - downloadType: 'single' - downloadPath: '$(System.ArtifactsDirectory)' - artifactName: 'nuget' - - - task: NuGetAuthenticate@0 - displayName: 'NuGet Authenticate' - - - task: NuGetCommand@2 - displayName: 'NuGet push' - inputs: - command: push - publishVstsFeed: 'InternalBuilds' - allowPackageConflicts: true - - - task: DownloadBuildArtifacts@0 - inputs: - buildType: 'current' - downloadType: 'single' - downloadPath: '$(build.artifactStagingDirectory)' - artifactName: 'symbols' - - - task: PublishSymbols@2 - displayName: 'Publish Symbols' - inputs: - symbolsFolder: '$(build.artifactStagingDirectory)/symbols' - searchPattern: '**/*.pdb' - symbolServerType: 'TeamServices' - indexSources: false # done in build step diff --git a/build/ci/run-e2e-features-enabled-tests.yml b/build/ci/run-e2e-features-enabled-tests.yml deleted file mode 100644 index 6864fe0346..0000000000 --- a/build/ci/run-e2e-features-enabled-tests.yml +++ /dev/null @@ -1,38 +0,0 @@ -jobs: -- job: SetupAndRun - displayName: 'Feature-Specific E2E Tests' - pool: - vmImage: 'ubuntu-latest' - steps: - - task: UseDotNet@2 - displayName: 'Use .Net Core sdk' - inputs: - useGlobalJson: true - - - task: AzurePowerShell@5 - displayName: 'Set Secret Variables' - inputs: - azureSubscription: $(azureSubscriptionName) - azurePowerShellVersion: latestVersion - ScriptType: inlineScript - Inline: | - $secrets = Get-AzKeyVaultSecret -VaultName $(deploymentName)-ts - - foreach($secret in $secrets) - { - $environmentVariableName = $secret.Name.Replace("--","_") - $secretValue = Get-AzKeyVaultSecret -VaultName $(deploymentName)-ts -Name $secret.Name -AsPlainText - Write-Host "##vso[task.setvariable variable=$environmentVariableName]$secretValue" - } - - - bash: | - echo "##vso[task.setvariable variable=testEnvironmentUrl]$(testServerFeaturesEnabledUrl)" - echo "##vso[task.setvariable variable=Resource]$(testServerFeaturesEnabledUrl)" - echo "##vso[task.setvariable variable=security_scope]$(testApplicationScope)" - echo "##vso[task.setvariable variable=security_resource]$(testApplicationResource)" - echo "##vso[task.setvariable variable=security_enabled]true" - - dotnet dev-certs https - displayName: 'Setup Authentication' - - - template: ../common/run-e2e-features-enabled-tests.yml diff --git a/build/ci/run-e2e-tests.yml b/build/ci/run-e2e-tests.yml deleted file mode 100644 index 2eb4ede4db..0000000000 --- a/build/ci/run-e2e-tests.yml +++ /dev/null @@ -1,61 +0,0 @@ -jobs: -- job: SetupAndRun - displayName: 'E2E Tests' - pool: - vmImage: 'ubuntu-latest' - steps: - - task: UseDotNet@2 - displayName: 'Use .Net Core sdk' - inputs: - useGlobalJson: true - - - task: AzurePowerShell@5 - displayName: 'Set Secret Variables' - inputs: - azureSubscription: $(azureSubscriptionName) - azurePowerShellVersion: latestVersion - ScriptType: inlineScript - Inline: | - $secrets = Get-AzKeyVaultSecret -VaultName $(deploymentName)-ts - - foreach($secret in $secrets) - { - $environmentVariableName = $secret.Name.Replace("--","_") - $secretValue = Get-AzKeyVaultSecret -VaultName $(deploymentName)-ts -Name $secret.Name -AsPlainText - Write-Host "##vso[task.setvariable variable=$environmentVariableName]$secretValue" - } - - - task: AzurePowerShell@5 - displayName: 'Create Azure Storage SAS Token' - inputs: - azureSubscription: $(azureSubscriptionName) - azurePowerShellVersion: latestVersion - ScriptType: inlineScript - Inline: | - $keys = Get-AzStorageAccountKey -Name '$(azureStorageAccountName)' -ResourceGroupName '$(deploymentName)' - $primaryKey = $keys[0].Value - - $start = Get-Date - $end = $start.AddHours(1) - $cxt = New-AzStorageContext -StorageAccountName '$(azureStorageAccountName)' -StorageAccountKey $primaryKey - $token = New-AzStorageAccountSASToken -Service Blob -ResourceType Container,Object -Permission 'rwdl' -Start $start -ExpiryTime $end -Context $cxt - $connectionString = "BlobEndpoint=https://$(azureStorageAccountName).blob.core.windows.net;SharedAccessSignature=$token" - - Write-Host "##vso[task.setvariable variable=Tests__Export__ConnectionString]$connectionString" - - - bash: | - echo "##vso[task.setvariable variable=testEnvironmentUrl]$(testServerUrl)" - echo "##vso[task.setvariable variable=Resource]$(testServerUrl)" - echo "##vso[task.setvariable variable=security_scope]$(testApplicationScope)" - echo "##vso[task.setvariable variable=security_resource]$(testApplicationResource)" - echo "##vso[task.setvariable variable=security_enabled]true" - - dotnet dev-certs https - displayName: 'Setup Authentication' - - - template: ../common/run-e2e-tests.yml - parameters: - externalStorageTests: false - - variables: - azureStorageAccountName: $[ stageDependencies.DeployTestEnvironment.Provision.outputs['deploy.azureStorageAccountName'] ] diff --git a/build/ci/run-integration-tests.yml b/build/ci/run-integration-tests.yml deleted file mode 100644 index c1eb434870..0000000000 --- a/build/ci/run-integration-tests.yml +++ /dev/null @@ -1,27 +0,0 @@ -jobs: -- job: SetupAndRun - displayName: 'Integration Tests' - pool: - vmImage: 'ubuntu-latest' - steps: - - task: UseDotNet@2 - displayName: 'Use .Net Core sdk' - inputs: - useGlobalJson: true - - - task: AzurePowerShell@5 - displayName: 'Prepare for Testing' - inputs: - azureSubscription: $(azureSubscriptionName) - azurePowerShellVersion: latestVersion - ScriptType: InlineScript - Inline: | - $sqlConnectionString = Get-AzKeyVaultSecret -VaultName $(deploymentName) -Name "SqlServerConnectionString" -AsPlainText - $blobConnectionString = Get-AzKeyVaultSecret -VaultName $(deploymentName) -Name "AzureStorageConnectionString" -AsPlainText - - Write-Host "##vso[task.setvariable variable=BlobStore__ConnectionString]$blobConnectionString" - Write-Host "##vso[task.setvariable variable=SqlServer__ConnectionString]$sqlConnectionString" - - dotnet dev-certs https - - - template: ../common/run-integration-tests.yml diff --git a/build/ci/variables.yml b/build/ci/variables.yml deleted file mode 100644 index 8e1ec2f8d8..0000000000 --- a/build/ci/variables.yml +++ /dev/null @@ -1,17 +0,0 @@ -variables: - deploymentName: 'dcm-ci-permanent' - testServerUrl: 'https://$(deploymentName).azurewebsites.net/' - testServerFeaturesEnabledUrl: 'https://$(deploymentName)-featuresenabled.azurewebsites.net/' - testApplicationScope: 'https://$(deploymentName).resoluteopensource.onmicrosoft.com/.default' - testApplicationResource: 'https://$(deploymentName).resoluteopensource.onmicrosoft.com' - resourceGroupName: $(deploymentName) - resourceGroupRegion: 'southcentralus' - appServicePlanResourceGroup: 'msh-dicom-pr' - appServicePlanName: $(appServicePlanResourceGroup)-$(resourceGroupRegion) - azureServiceConnectionOid: '5e9db4f6-b680-4408-a85b-af0ad8ef185d' - azureSubscriptionName: 'Dicom OSS' - buildConfiguration: 'Release' - imageTag: '$(build.BuildNumber)' - azureContainerRegistry: 'dicomoss.azurecr.io' - deleteDataOnStartup: 'false' - skipNugetSecurityAnalysis: 'true' # NuGet config contains multiple feeds but meets exception criteria diff --git a/build/common/analyze.yml b/build/common/analyze.yml deleted file mode 100644 index 70bf675c6f..0000000000 --- a/build/common/analyze.yml +++ /dev/null @@ -1,97 +0,0 @@ -parameters: - analyzeARMTemplates: true - analyzeBinaries: true - analyzePackages: true - runAntiMalware: true - credScanDirectory: '$(Build.SourcesDirectory)' - -steps: -- ${{ if eq(parameters.analyzeBinaries, 'true') }}: - - task: DownloadBuildArtifacts@0 - displayName: 'Download DICOM Binaries' - inputs: - buildType: 'current' - downloadType: 'single' - downloadPath: '$(Agent.TempDirectory)/artifacts' - artifactName: 'deploy' - -- ${{ if eq(parameters.analyzePackages, 'true') }}: - - task: DownloadBuildArtifacts@0 - displayName: 'Download DICOM NuGet Packages' - inputs: - buildType: 'current' - downloadType: 'single' - downloadPath: '$(Build.SourcesDirectory)/artifacts' - artifactName: 'nuget' - -- ${{ if eq(parameters.analyzeBinaries, 'true') }}: - - task: ExtractFiles@1 - displayName: 'Extract DICOM Web Server Binaries' - inputs: - archiveFilePatterns: '$(Agent.TempDirectory)/artifacts/deploy/Microsoft.Health.Dicom.Web.zip' - destinationFolder: '$(Build.SourcesDirectory)/artifacts/web' - -- ${{ if eq(parameters.runAntiMalware, 'true') }}: - - task: AntiMalware@4 - inputs: - InputType: 'Basic' - ScanType: 'CustomScan' - FileDirPath: '$(Build.SourcesDirectory)' - EnableServices: true - TreatSignatureUpdateFailureAs: 'Standard' - SignatureFreshness: 'OneDay' - TreatStaleSignatureAs: 'Error' - -- ${{ if eq(parameters.analyzeARMTemplates, 'true') }}: - - task: Armory@2 - inputs: - targetDirectory: '$(Build.SourcesDirectory)\samples\templates' - targetFiles: 'f|*.json' - excludePassesFromLog: false - -- task: CredScan@3 - inputs: - scanFolder: ${{ parameters.credScanDirectory }} - outputFormat: 'sarif' - suppressionsFile: 'CredScanSuppressions.json' - verboseOutput: true - -- task: SdtReport@2 - inputs: - GdnExportAllTools: false - GdnExportGdnToolArmory: ${{ eq(parameters.analyzeARMTemplates, 'true') }} - GdnExportGdnToolCredScan: true - -- task: PublishSecurityAnalysisLogs@3 - inputs: - ArtifactName: 'CodeAnalysisLogs' - ArtifactType: 'Container' - AllTools: false - AntiMalware: ${{ eq(parameters.runAntiMalware, 'true') }} - APIScan: false - Armory: ${{ eq(parameters.analyzeARMTemplates, 'true') }} - Bandit: false - BinSkim: false - CodesignValidation: false - CredScan: true - CSRF: false - ESLint: false - Flawfinder: false - FortifySCA: false - FxCop: false - ModernCop: false - MSRD: false - PoliCheck: false - RoslynAnalyzers: false - SDLNativeRules: false - Semmle: false - SpotBugs: false - TSLint: false - WebScout: false - ToolLogsNotFoundAction: 'Standard' - -- task: PostAnalysis@2 - inputs: - GdnBreakAllTools: false - GdnBreakGdnToolArmory: ${{ eq(parameters.analyzeARMTemplates, 'true') }} - GdnBreakGdnToolCredScan: true diff --git a/build/common/build-dotnet.yml b/build/common/build-dotnet.yml deleted file mode 100644 index 0eb720b076..0000000000 --- a/build/common/build-dotnet.yml +++ /dev/null @@ -1,41 +0,0 @@ -parameters: - packageArtifacts: true - packageNugets: true - -steps: - - task: UseDotNet@2 - displayName: 'Use .NET 6 sdk' - inputs: - version: 6.x - - - task: UseDotNet@2 - displayName: 'Use .NET sdk' - inputs: - useGlobalJson: true - - - task: DotNetCoreCLI@2 - displayName: 'dotnet build $(buildConfiguration)' - inputs: - command: 'build' - projects: '**/*.csproj' - arguments: '--configuration $(buildConfiguration) -warnaserror -p:AssemblyVersion="$(assemblySemVer)" -p:FileVersion="$(assemblySemFileVer)" -p:InformationalVersion="$(informationalVersion)" -p:ContinuousIntegrationBuild=true' - - - task: DotNetCoreCLI@2 - displayName: 'dotnet test UnitTests' - inputs: - command: test - projects: '**/*UnitTests/*.csproj' - arguments: '--configuration $(buildConfiguration) --no-build' - - - task: ComponentGovernanceComponentDetection@0 - inputs: - scanType: 'Register' - verbosity: 'Verbose' - alertWarningLevel: 'High' - failOnAlert: true - - - ${{ if eq(parameters.packageArtifacts, 'true') }}: - - template: package.yml - - - ${{ if eq(parameters.packageNugets, 'true') }}: - - template: package-nugets.yml diff --git a/build/common/build-electron.yml b/build/common/build-electron.yml deleted file mode 100644 index fe6789e7ce..0000000000 --- a/build/common/build-electron.yml +++ /dev/null @@ -1,18 +0,0 @@ -steps: - - task: NodeTool@0 - displayName: 'Install Node.js' - inputs: - versionSpec: '17.x' - - # TODO: Add validation - - script: npm ci - displayName: 'npm ci' - workingDirectory: '$(Build.Repository.LocalPath)/tools/dicom-web-electron' - - - task: ComponentGovernanceComponentDetection@0 - displayName: 'Component Governance' - inputs: - scanType: 'Register' - verbosity: 'Verbose' - alertWarningLevel: 'High' - failOnAlert: true diff --git a/build/common/docker-build-push.yml b/build/common/docker-build-push.yml deleted file mode 100644 index 735875aa89..0000000000 --- a/build/common/docker-build-push.yml +++ /dev/null @@ -1,131 +0,0 @@ -# DESCRIPTION: -# Builds and pushes a docker image for dicom-server and dicom-cast - -parameters: - - name: tag - type: string - - name: push - type: boolean - default: true - -steps: - - ${{ if eq(parameters.push, true) }}: - - task: Docker@2 - displayName: Login - inputs: - command: login - containerRegistry: '$(azureContainerRegistry)' - - - task: Docker@2 - displayName: 'Build dicom-server' - inputs: - command: 'build' - containerRegistry: '$(azureContainerRegistry)' - Dockerfile: 'src/Microsoft.Health.Dicom.Web/Dockerfile' - buildContext: '$(Build.Repository.LocalPath)' - arguments: '--build-arg BUILD_CONFIGURATION=Release --build-arg CONTINUOUS_INTEGRATION_BUILD=true' - repository: dicom-server - tags: ${{ parameters.tag }} - - - task: Docker@2 - displayName: 'Build dicom-cast' - inputs: - command: 'build' - containerRegistry: '$(azureContainerRegistry)' - Dockerfile: 'converter/dicom-cast/src/Microsoft.Health.DicomCast.Hosting/Dockerfile' - buildContext: '$(Build.Repository.LocalPath)' - arguments: '--build-arg BUILD_CONFIGURATION=Release --build-arg CONTINUOUS_INTEGRATION_BUILD=true' - repository: dicom-cast - tags: | - ${{ parameters.tag }} - latest - - - task: Docker@2 - displayName: 'Build dicom-functions' - inputs: - command: 'build' - containerRegistry: '$(azureContainerRegistry)' - Dockerfile: 'src/Microsoft.Health.Dicom.Functions.App/Docker/Dockerfile' - buildContext: '$(Build.Repository.LocalPath)' - arguments: '--build-arg BUILD_CONFIGURATION=Release --build-arg CONTINUOUS_INTEGRATION_BUILD=true' - repository: dicom-functions - tags: ${{ parameters.tag }} - - - task: Docker@2 - displayName: 'Build dicom-uploader' - inputs: - command: 'build' - containerRegistry: '$(azureContainerRegistry)' - Dockerfile: 'tools/uploader-function/src/DicomUploaderFunction/Dockerfile' - buildContext: '$(Build.Repository.LocalPath)' - arguments: '--build-arg BUILD_CONFIGURATION=Release --build-arg CONTINUOUS_INTEGRATION_BUILD=true' - repository: dicom-uploader - tags: | - ${{ parameters.tag }} - latest - - # Build SQL for the sake of component governance - - task: Docker@2 - displayName: 'Build SQL Server Image' - inputs: - command: 'build' - containerRegistry: '$(azureContainerRegistry)' - Dockerfile: 'docker/sql/Dockerfile' - buildContext: '$(Build.Repository.LocalPath)' - repository: mssql-server-focal - tags: ${{ parameters.tag }} - - - task: ComponentGovernanceComponentDetection@0 - inputs: - scanType: 'Register' - verbosity: 'Verbose' - alertWarningLevel: 'High' - failOnAlert: true - - - ${{ if eq(parameters.push, true) }}: - - task: Docker@2 - displayName: 'Push dicom-server' - inputs: - command: 'push' - containerRegistry: '$(azureContainerRegistry)' - repository: dicom-server - tags: ${{ parameters.tag }} - - - ${{ if eq(parameters.push, true) }}: - - task: Docker@2 - displayName: 'Push dicom-cast' - inputs: - command: 'push' - containerRegistry: '$(azureContainerRegistry)' - repository: dicom-cast - tags: | - ${{ parameters.tag }} - latest - - - ${{ if eq(parameters.push, true) }}: - - task: Docker@2 - displayName: 'Push dicom-functions' - inputs: - command: 'push' - containerRegistry: '$(azureContainerRegistry)' - repository: dicom-functions - tags: ${{ parameters.tag }} - - - ${{ if eq(parameters.push, true) }}: - - task: Docker@2 - displayName: 'Push dicom-uploader' - inputs: - command: 'push' - containerRegistry: '$(azureContainerRegistry)' - repository: dicom-uploader - tags: | - ${{ parameters.tag }} - latest - - - ${{ if eq(parameters.push, true) }}: - - task: Docker@2 - displayName: Logout - inputs: - command: logout - containerRegistry: '$(azureContainerRegistry)' - condition: always() diff --git a/build/common/package-nugets.yml b/build/common/package-nugets.yml deleted file mode 100644 index cb2afcf151..0000000000 --- a/build/common/package-nugets.yml +++ /dev/null @@ -1,38 +0,0 @@ -steps: - # Package nugets - - task: DotNetCoreCLI@2 - displayName: 'dotnet pack nugets' - inputs: - command: pack - configuration: '$(buildConfiguration)' - packagesToPack: '**/*.csproj;!**/*.UnitTests.csproj' - packDirectory: '$(build.artifactStagingDirectory)/nupkgs' - versioningScheme: byEnvVar - versionEnvVar: 'nuGetVersion' - nobuild: true - - - task: PublishBuildArtifacts@1 - displayName: 'publish nuget artifacts' - inputs: - pathtoPublish: '$(build.artifactStagingDirectory)/nupkgs' - artifactName: 'nuget' - publishLocation: 'container' - - - task: CopyFiles@2 - displayName: 'copy symbols' - inputs: - sourceFolder: '$(build.sourcesDirectory)' - contents: | - **/*.pdb - !**/*.UnitTests.pdb - targetFolder: '$(build.artifactStagingDirectory)/symbols' - cleanTargetFolder: true - flattenFolders: true - overWrite: true - - - task: PublishBuildArtifacts@1 - displayName: 'publish symbol artifacts' - inputs: - pathtoPublish: '$(build.artifactStagingDirectory)/symbols' - artifactName: 'symbols' - publishLocation: 'container' diff --git a/build/common/package.yml b/build/common/package.yml deleted file mode 100644 index 6b29fcbe92..0000000000 --- a/build/common/package.yml +++ /dev/null @@ -1,80 +0,0 @@ -steps: - - # Package web - - - task: DotNetCoreCLI@2 - displayName: 'dotnet publish web' - inputs: - command: publish - projects: '**/Microsoft.Health.Dicom.Web.csproj' - arguments: '--configuration $(buildConfiguration) --output $(build.artifactStagingDirectory)/web --no-build' - publishWebProjects: false - - - task: DotNetCoreCLI@2 - displayName: 'dotnet publish functions' - inputs: - command: publish - projects: '**/Microsoft.Health.Dicom.Functions.App.csproj' - arguments: '--configuration $(buildConfiguration) --output $(build.artifactStagingDirectory)/functions --no-build' - publishWebProjects: false - - - task: DotNetCoreCLI@2 - displayName: 'dotnet publish Integration Tests' - inputs: - command: publish - projects: 'test/**/*.csproj' - arguments: '--configuration $(buildConfiguration) --output "$(build.binariesdirectory)/IntegrationTests" --no-build' - publishWebProjects: false - zipAfterPublish: false - - # Publish artifacts - - - task: PublishBuildArtifacts@1 - displayName: 'publish web artifacts' - inputs: - pathToPublish: '$(build.artifactStagingDirectory)/web' - artifactName: 'deploy' - artifactType: 'container' - - - task: PublishBuildArtifacts@1 - displayName: 'publish function artifacts' - inputs: - pathToPublish: '$(build.artifactStagingDirectory)/functions' - artifactName: 'deploy' - artifactType: 'container' - - - task: PublishBuildArtifacts@1 - displayName: 'publish samples' - inputs: - pathToPublish: './samples/' - artifactName: 'deploy' - artifactType: 'container' - - - task: PublishBuildArtifacts@1 - displayName: 'publish dicom-cast samples' - inputs: - pathToPublish: './converter/dicom-cast/samples/' - artifactName: 'deploy-dicom-cast' - artifactType: 'container' - - - task: PublishBuildArtifacts@1 - displayName: 'publish global.json' - inputs: - pathToPublish: './global.json' - artifactName: 'deploy' - artifactType: 'container' - - - task: PublishBuildArtifacts@1 - displayName: 'publish test configuration jsons' - enabled: false - inputs: - pathToPublish: './test/configuration/' - artifactName: 'deploy' - artifactType: 'container' - - - task: PublishBuildArtifacts@1 - displayName: 'publish Integration Tests' - inputs: - pathToPublish: '$(build.binariesdirectory)/IntegrationTests' - artifactName: 'IntegrationTests' - artifactType: 'container' diff --git a/build/common/run-e2e-connected-store-tests.yml b/build/common/run-e2e-connected-store-tests.yml deleted file mode 100644 index 66c7c42e36..0000000000 --- a/build/common/run-e2e-connected-store-tests.yml +++ /dev/null @@ -1,25 +0,0 @@ -parameters: - externalStorageTests: true - -steps: -- task: DownloadBuildArtifacts@0 - inputs: - buildType: 'current' - downloadType: 'single' - downloadPath: '$(System.ArtifactsDirectory)' - artifactName: 'IntegrationTests' - -- script: dotnet test "Microsoft.Health.Dicom.Web.Tests.E2E.dll" --filter "Category!=bvt-dp" --logger trx --results-directory "$(Agent.TempDirectory)/TestResults" -e DicomServer__Features__EnableExternalStore="true" - displayName: 'dotnet test Microsoft.Health.Dicom.Web.Tests.E2E.dll with EnableExternalStore' - workingDirectory: '$(System.ArtifactsDirectory)/IntegrationTests/Microsoft.Health.Dicom.Web.Tests.E2E' - condition: eq(${{ parameters.externalStorageTests }}, true) - -- task: PublishTestResults@2 - displayName: 'Publish Connected Store Test Results' - condition: succeededOrFailed() - inputs: - testResultsFormat: 'VSTest' - testResultsFiles: '*.trx' - searchFolder: '$(Agent.TempDirectory)/TestResults' - testRunTitle: 'E2E Connected Store Tests' - buildConfiguration: '$(buildConfiguration)' diff --git a/build/common/run-e2e-features-enabled-connected-store-tests.yml b/build/common/run-e2e-features-enabled-connected-store-tests.yml deleted file mode 100644 index 24766ec354..0000000000 --- a/build/common/run-e2e-features-enabled-connected-store-tests.yml +++ /dev/null @@ -1,21 +0,0 @@ -steps: -- task: DownloadBuildArtifacts@0 - inputs: - buildType: 'current' - downloadType: 'single' - downloadPath: '$(System.ArtifactsDirectory)' - artifactName: 'IntegrationTests' - -- script: dotnet test "Microsoft.Health.Dicom.Web.Tests.E2E.dll" --filter "Category=bvt-dp" --logger trx --results-directory "$(Agent.TempDirectory)/TestResults" -e DicomServer__Features__EnableExternalStore="true" - displayName: 'dotnet test Microsoft.Health.Dicom.Web.Tests.E2E.dll with EnableExternalStore' - workingDirectory: '$(System.ArtifactsDirectory)/IntegrationTests/Microsoft.Health.Dicom.Web.Tests.E2E' - -- task: PublishTestResults@2 - displayName: 'Publish Connected Store Test Results' - condition: succeededOrFailed() - inputs: - testResultsFormat: 'VSTest' - testResultsFiles: '*.trx' - searchFolder: '$(Agent.TempDirectory)/TestResults' - testRunTitle: 'Partitioned Connected Store E2E Tests' - buildConfiguration: '$(buildConfiguration)' diff --git a/build/common/run-e2e-features-enabled-tests.yml b/build/common/run-e2e-features-enabled-tests.yml deleted file mode 100644 index 3334b6ae7a..0000000000 --- a/build/common/run-e2e-features-enabled-tests.yml +++ /dev/null @@ -1,21 +0,0 @@ -steps: -- task: DownloadBuildArtifacts@0 - inputs: - buildType: 'current' - downloadType: 'single' - downloadPath: '$(System.ArtifactsDirectory)' - artifactName: 'IntegrationTests' - -- script: dotnet test "Microsoft.Health.Dicom.Web.Tests.E2E.dll" --filter "Category=bvt-dp" --logger trx --results-directory "$(Agent.TempDirectory)/TestResults" -e DicomServer__Features__EnableExternalStore="false" - displayName: 'dotnet test Microsoft.Health.Dicom.Web.Tests.E2E.dll' - workingDirectory: '$(System.ArtifactsDirectory)/IntegrationTests/Microsoft.Health.Dicom.Web.Tests.E2E' - -- task: PublishTestResults@2 - displayName: 'Publish Test Results' - condition: succeededOrFailed() - inputs: - testResultsFormat: 'VSTest' - testResultsFiles: '*.trx' - searchFolder: '$(Agent.TempDirectory)/TestResults' - testRunTitle: 'Partitioned E2E Tests' - buildConfiguration: '$(buildConfiguration)' diff --git a/build/common/run-e2e-tests.yml b/build/common/run-e2e-tests.yml deleted file mode 100644 index 0d4ad7653f..0000000000 --- a/build/common/run-e2e-tests.yml +++ /dev/null @@ -1,24 +0,0 @@ -parameters: - externalStorageTests: true - -steps: -- task: DownloadBuildArtifacts@0 - inputs: - buildType: 'current' - downloadType: 'single' - downloadPath: '$(System.ArtifactsDirectory)' - artifactName: 'IntegrationTests' - -- script: dotnet test "Microsoft.Health.Dicom.Web.Tests.E2E.dll" --filter "Category!=bvt-dp" --logger trx --results-directory "$(Agent.TempDirectory)/TestResults" -e DicomServer__Features__EnableExternalStore="false" - displayName: 'dotnet test Microsoft.Health.Dicom.Web.Tests.E2E.dll' - workingDirectory: '$(System.ArtifactsDirectory)/IntegrationTests/Microsoft.Health.Dicom.Web.Tests.E2E' - -- task: PublishTestResults@2 - displayName: 'Publish Test Results' - condition: succeededOrFailed() - inputs: - testResultsFormat: 'VSTest' - testResultsFiles: '*.trx' - searchFolder: '$(Agent.TempDirectory)/TestResults' - testRunTitle: 'E2E Tests' - buildConfiguration: '$(buildConfiguration)' diff --git a/build/common/run-integration-tests.yml b/build/common/run-integration-tests.yml deleted file mode 100644 index 2495d9103e..0000000000 --- a/build/common/run-integration-tests.yml +++ /dev/null @@ -1,20 +0,0 @@ -steps: -- task: DownloadBuildArtifacts@0 - inputs: - buildType: 'current' - downloadType: 'single' - downloadPath: '$(System.ArtifactsDirectory)' - artifactName: 'IntegrationTests' - -- script: dotnet test "Microsoft.Health.Dicom.Tests.Integration.dll" --logger trx --results-directory "$(Agent.TempDirectory)/TestResults" - displayName: 'dotnet test Microsoft.Health.Dicom.Tests.Integration.dll' - workingDirectory: '$(System.ArtifactsDirectory)/IntegrationTests/Microsoft.Health.Dicom.Tests.Integration' - -- task: PublishTestResults@2 - displayName: 'Publish Test Results' - condition: succeededOrFailed() - inputs: - testResultsFormat: 'VSTest' - testResultsFiles: '*.trx' - searchFolder: '$(Agent.TempDirectory)/TestResults' - buildConfiguration: '$(buildConfiguration)' diff --git a/build/common/scripts/CheckForBreakingAPISwaggerChanges.ps1 b/build/common/scripts/CheckForBreakingAPISwaggerChanges.ps1 deleted file mode 100644 index eae7cd6ada..0000000000 --- a/build/common/scripts/CheckForBreakingAPISwaggerChanges.ps1 +++ /dev/null @@ -1,37 +0,0 @@ -<# -.SYNOPSIS -Generates the OpenApi doc for the specified version and compares it with the baseline to make sure no breaking changes are introduced -Run script from root of this repository -.Parameter SwaggerDir -Swagger directory path from root of this repository. Ex: 'swagger' -.PARAMETER Versions -Api versions to generate the OpenApiDoc for and compare with baseline -#> - -param( - [string]$SwaggerDir, - - [String[]]$Versions -) - -$ErrorActionPreference = 'Stop' -$container="openapitools/openapi-diff:latest@sha256:442d61387d4d3c5bff282f1eb30decffa87c8c4acae77e6ac3f815b1f63672ea" - -if (Test-Path "$SwaggerDir/FromMain") { Remove-Item -Recurse -Force "$SwaggerDir/FromMain" } -mkdir "$SwaggerDir/FromMain" - -foreach ($Version in $Versions) -{ - $new=(Join-Path -Path "$SwaggerDir" -ChildPath "$Version/swagger.yaml") - $old=(Join-Path -Path "$SwaggerDir" -ChildPath "/FromMain/$Version.yaml") - - $SwaggerOnMain="https://raw.githubusercontent.com/microsoft/dicom-server/main/swagger/$Version/swagger.yaml" - Invoke-WebRequest -Uri $SwaggerOnMain -OutFile $old - - write-host "Running comparison with baseline for version $Version" - Write-Host "old: $old" - Write-Host "new: $new" - docker run --rm -t -v "${pwd}/${SwaggerDir}:/${SwaggerDir}:ro" $container /$old /$new --fail-on-incompatible -} - -Remove-Item -Recurse -Force "$SwaggerDir/FromMain" diff --git a/build/common/scripts/CheckForSwaggerChanges.ps1 b/build/common/scripts/CheckForSwaggerChanges.ps1 deleted file mode 100644 index a9ca4bad5b..0000000000 --- a/build/common/scripts/CheckForSwaggerChanges.ps1 +++ /dev/null @@ -1,52 +0,0 @@ -<# -.SYNOPSIS -Generates the OpenApi doc for the specified version and compares it with the checked in version to ensure it is up to date. -.Parameter SwaggerDir -The working directory -.PARAMETER AssemblyDir -Path for the web projects dll -.PARAMETER Versions -Api versions to compare with -#> - -param( - [string]$SwaggerDir, - - [string]$AssemblyDir, - - [String[]]$Versions -) - -$ErrorActionPreference = 'Stop' # ensure script behaves same locally as within default pwsh ado task -dotnet tool restore - -Write-Host "Using swagger version ..." -dotnet tool list | Select-String "swashbuckle" - -Write-Host "Testing that swagger will work ..." -dotnet swagger - -foreach ($Version in $Versions) -{ - Write-Host "Ensuring directory path exists for swagger api version $Version" - $ProjectSwaggerDir=".\swagger\$Version" - if (!(Test-Path $ProjectSwaggerDir -PathType Container)) { - New-Item -ItemType Directory -Force -Path $ProjectSwaggerDir - Write-Host "Directory $ProjectSwaggerDir did not exist. Directory created." - } - - $WritePath=(Join-Path -Path "$SwaggerDir" -ChildPath "$Version.yaml") - Write-Host "Generating swagger yaml file for $Version to path $WritePath" - - dotnet swagger tofile --yaml --output $WritePath "$AssemblyDir" $Version - - Write-Host "Comparing generated swagger with what was checked in ..." - $HasDifferences = (Compare-Object -ReferenceObject (Get-Content -Path $WritePath) -DifferenceObject (Get-Content -Path "$ProjectSwaggerDir\swagger.yaml")) - - if ($HasDifferences){ - Write-Host $HasDifferences - throw "The swagger yaml checked in with this PR is not up to date with code. Please build the sln, which will trigger a hook to autogenerate these files on your behalf. Differences shown above." - } else{ - Write-Host "Swagger checked in with this PR is up to date with code." - } -} diff --git a/build/common/update-semver.yml b/build/common/update-semver.yml deleted file mode 100644 index c4df95ae82..0000000000 --- a/build/common/update-semver.yml +++ /dev/null @@ -1,14 +0,0 @@ -steps: -- task: gitversion/setup@0 - displayName: 'Setup GitVersion' - inputs: - versionSpec: '5.x' - -# All variables from the GitVersion task are prefixed by "GitVersion." (eg. GitVersion.SemVer) -- task: gitversion/execute@0 - name: 'DicomVersion' - displayName: 'Run GitVersion' - inputs: - configFilePath: 'GitVersion.yml' - targetPath: '$(Build.SourcesDirectory)' - useConfigFile: true diff --git a/build/common/versioning.yml b/build/common/versioning.yml deleted file mode 100644 index 4fbab300f4..0000000000 --- a/build/common/versioning.yml +++ /dev/null @@ -1,47 +0,0 @@ -jobs: -- job: OpenApiDiff - pool: - vmImage: 'ubuntu-latest' - steps: - - task: UseDotNet@2 - displayName: 'Use .NET 7 sdk' - inputs: - version: 7.x - - - task: UseDotNet@2 - displayName: 'Use .NET sdk' - inputs: - useGlobalJson: true - - - task: DownloadBuildArtifacts@0 - inputs: - buildType: 'current' - downloadType: 'single' - downloadPath: '$(System.ArtifactsDirectory)' - artifactName: 'deploy' - - - task: ExtractFiles@1 - displayName: 'Extract Web zip' - inputs: - archiveFilePatterns: '$(System.ArtifactsDirectory)/deploy/Microsoft.Health.Dicom.Web.zip' - destinationFolder: '$(System.ArtifactsDirectory)/deploy/webArtifacts' - - - task: PowerShell@2 - displayName: 'Check for latest Swagger changes' - inputs: - pwsh: true - filepath: './build/common/scripts/CheckForSwaggerChanges.ps1' - arguments: > - -SwaggerDir '$(System.DefaultWorkingDirectory)/swagger' - -AssemblyDir '$(System.ArtifactsDirectory)/deploy/webArtifacts/Microsoft.Health.Dicom.Web.dll' - -Version 'v1-prerelease','v1','v2' - - - task: PowerShell@2 - displayName: 'Check for breaking API / Swagger changes' - enabled: false # will re-enable once v2 swagger exists on main - inputs: - pwsh: true - filepath: './build/common/scripts/CheckForBreakingAPISwaggerChanges.ps1' - arguments: > - -SwaggerDir 'swagger' - -Version 'v1-prerelease','v1','v2' diff --git a/build/pr/run-e2e-features-enabled-tests.yml b/build/pr/run-e2e-features-enabled-tests.yml deleted file mode 100644 index 49dab0c2e1..0000000000 --- a/build/pr/run-e2e-features-enabled-tests.yml +++ /dev/null @@ -1,64 +0,0 @@ -jobs: -- job: SetupAndRun - displayName: 'Feature-Specific E2E Tests' - pool: - vmImage: 'ubuntu-latest' - steps: - - task: UseDotNet@2 - displayName: 'Use .Net Core sdk' - inputs: - useGlobalJson: true - - - script: ContinuousIntegrationBuild=true docker-compose -p healthcare -f docker-compose.yml -f docker-compose.features.yml up --build -d - displayName: 'Run docker-compose' - workingDirectory: 'docker' - - - bash: for i in {1..12}; do curl -fsS "$(testEnvironmentUrl)health/check" > /dev/null && exit 0 || sleep 5; done; exit 1 - displayName: 'Wait for DICOM Server' - - - bash: for i in {1..12}; do curl -fsS "$(testFunctionsUrl)" > /dev/null && exit 0 || sleep 5; done; exit 1 - displayName: 'Wait for DICOM Functions' - - - template: ../common/run-e2e-features-enabled-tests.yml - - - script: docker-compose -p healthcare -f docker-compose.yml -f docker-compose.features.yml logs -t - displayName: 'docker-compose logs' - workingDirectory: 'docker' - condition: always() - - - script: docker-compose -p healthcare -f docker-compose.yml -f docker-compose.features.yml rm -s -f - displayName: 'Stop docker-compose' - workingDirectory: 'docker' - condition: always() - -- job: SetupAndRunConnectedStore - displayName: 'Feature-Specific E2E Connected Store Tests' - pool: - vmImage: 'ubuntu-latest' - steps: - - task: UseDotNet@2 - displayName: 'Use .Net Core sdk' - inputs: - useGlobalJson: true - - - script: ContinuousIntegrationBuild=true docker-compose -p healthcare -f docker-compose.yml -f docker-compose.features.yml up --build -d - displayName: 'Run docker-compose' - workingDirectory: 'docker' - - - bash: for i in {1..12}; do curl -fsS "$(testEnvironmentUrl)health/check" > /dev/null && exit 0 || sleep 5; done; exit 1 - displayName: 'Wait for DICOM Server' - - - bash: for i in {1..12}; do curl -fsS "$(testFunctionsUrl)" > /dev/null && exit 0 || sleep 5; done; exit 1 - displayName: 'Wait for DICOM Functions' - - - template: ../common/run-e2e-features-enabled-connected-store-tests.yml - - - script: docker-compose -p healthcare -f docker-compose.yml -f docker-compose.features.yml logs -t - displayName: 'docker-compose logs' - workingDirectory: 'docker' - condition: always() - - - script: docker-compose -p healthcare -f docker-compose.yml -f docker-compose.features.yml rm -s -f - displayName: 'Stop docker-compose' - workingDirectory: 'docker' - condition: always() \ No newline at end of file diff --git a/build/pr/run-e2e-tests.yml b/build/pr/run-e2e-tests.yml deleted file mode 100644 index 33d5f81f08..0000000000 --- a/build/pr/run-e2e-tests.yml +++ /dev/null @@ -1,64 +0,0 @@ -jobs: -- job: SetupAndRun - displayName: 'E2E Tests' - pool: - vmImage: 'ubuntu-latest' - steps: - - task: UseDotNet@2 - displayName: 'Use .Net Core sdk' - inputs: - useGlobalJson: true - - - script: ContinuousIntegrationBuild=true docker-compose -p healthcare -f docker-compose.yml -f docker-compose.ports.azurite.yml up --build -d - displayName: 'Run docker-compose' - workingDirectory: 'docker' - - - bash: for i in {1..12}; do curl -fsS "$(testEnvironmentUrl)health/check" > /dev/null && exit 0 || sleep 5; done; exit 1 - displayName: 'Wait for DICOM Server' - - - bash: for i in {1..12}; do curl -fsS "$(testFunctionsUrl)" > /dev/null && exit 0 || sleep 5; done; exit 1 - displayName: 'Wait for DICOM Functions' - - - template: ../common/run-e2e-tests.yml - - - script: docker-compose -p healthcare -f docker-compose.yml logs -t - displayName: 'docker-compose logs' - workingDirectory: 'docker' - condition: always() - - - script: docker-compose -p healthcare -f docker-compose.yml rm -s -f - displayName: 'Stop docker-compose' - workingDirectory: 'docker' - condition: always() - -- job: SetupAndRunConnectedStore - displayName: 'E2E Connected Store Tests' - pool: - vmImage: 'ubuntu-latest' - steps: - - task: UseDotNet@2 - displayName: 'Use .Net Core sdk' - inputs: - useGlobalJson: true - - - script: ContinuousIntegrationBuild=true docker-compose -p healthcare -f docker-compose.yml -f docker-compose.ports.azurite.yml up --build -d - displayName: 'Run docker-compose' - workingDirectory: 'docker' - - - bash: for i in {1..12}; do curl -fsS "$(testEnvironmentUrl)health/check" > /dev/null && exit 0 || sleep 5; done; exit 1 - displayName: 'Wait for DICOM Server' - - - bash: for i in {1..12}; do curl -fsS "$(testFunctionsUrl)" > /dev/null && exit 0 || sleep 5; done; exit 1 - displayName: 'Wait for DICOM Functions' - - - template: ../common/run-e2e-connected-store-tests.yml - - - script: docker-compose -p healthcare -f docker-compose.yml logs -t - displayName: 'docker-compose logs' - workingDirectory: 'docker' - condition: always() - - - script: docker-compose -p healthcare -f docker-compose.yml rm -s -f - displayName: 'Stop docker-compose' - workingDirectory: 'docker' - condition: always() \ No newline at end of file diff --git a/build/pr/run-integration-tests.yml b/build/pr/run-integration-tests.yml deleted file mode 100644 index 257ce29d9d..0000000000 --- a/build/pr/run-integration-tests.yml +++ /dev/null @@ -1,34 +0,0 @@ -jobs: -- job: SetupAndRun - displayName: 'Integration Tests' - pool: - vmImage: 'ubuntu-latest' - steps: - - task: UseDotNet@2 - displayName: 'Use .Net Core sdk' - inputs: - useGlobalJson: true - - - bash: | - # Set Environment Variables - echo "##vso[task.setvariable variable=BlobStore__ConnectionString]UseDevelopmentStorage=true" - echo "##vso[task.setvariable variable=SqlServer__ConnectionString]Server=(local);Persist Security Info=False;User ID=sa;Password=L0ca1P@ssw0rd;MultipleActiveResultSets=False;Connection Timeout=30;TrustServerCertificate=true" - - # Start Azurite - docker run -p 10000:10000 -p 10001:10001 -p 10002:10002 -d --rm --name azure-storage mcr.microsoft.com/azure-storage/azurite:latest - - # Start SQL Server - docker build -t fulltext-mssql -f "docker/sql/Dockerfile" . - docker run -e "ACCEPT_EULA=Y" -e "SA_PASSWORD=L0ca1P@ssw0rd" -p 1433:1433 -d --rm --name sql-server fulltext-mssql - - # Wait for SQL to start - for i in {1..6}; do docker exec sql-server sh -c "/opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P L0ca1P@ssw0rd -Q 'SELECT * FROM INFORMATION_SCHEMA.TABLES'" && exit 0 || sleep 5; done; exit 1 - displayName: 'Start Docker Dependencies' - - - template: ../common/run-integration-tests.yml - - - script: | - docker stop azure-storage - docker stop sql-server - displayName: 'Stop Docker Dependencies' - condition: always() diff --git a/build/pr/variables.yml b/build/pr/variables.yml deleted file mode 100644 index 843de44108..0000000000 --- a/build/pr/variables.yml +++ /dev/null @@ -1,15 +0,0 @@ -variables: - prNumber: $(system.pullRequest.pullRequestNumber) - deploymentName: 'msh-dicom-pr-$(prNumber)' - testEnvironmentUrl: 'http://localhost:8080/' - testFunctionsUrl: 'http://localhost:7072/' - resourceGroupName: $(deploymentName) - resourceGroupRegion: 'southcentralus' - appServicePlanResourceGroup: 'msh-dicom-pr' - azureSubscriptionId: 'a1766500-6fd5-4f5c-8515-607798271014' - azureSubscriptionName: 'Dicom OSS' - azureContainerRegistry: 'dicomoss.azurecr.io' - buildConfiguration: 'Release' - imageTag: '$(build.BuildNumber)' - skipNugetSecurityAnalysis: 'true' # NuGet config contains multiple feeds but meets exception criteria - Tests__Export__Sink__ConnectionString: 'DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://azurite:10000/devstoreaccount1;' diff --git a/converter/dicom-cast/Microsoft.Health.DicomCast.sln b/converter/dicom-cast/Microsoft.Health.DicomCast.sln deleted file mode 100644 index c22619f085..0000000000 --- a/converter/dicom-cast/Microsoft.Health.DicomCast.sln +++ /dev/null @@ -1,82 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.2.32414.248 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{4EB555C8-F221-4663-B314-58AF0331A225}" - ProjectSection(SolutionItems) = preProject - ..\..\.editorconfig = ..\..\.editorconfig - ..\..\.globalconfig = ..\..\.globalconfig - ..\..\Directory.Build.props = ..\..\Directory.Build.props - ..\..\Directory.Packages.props = ..\..\Directory.Packages.props - ..\..\GitVersion.yml = ..\..\GitVersion.yml - ..\..\global.json = ..\..\global.json - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{E15E21A1-0707-4D98-AEDD-ACC7F59C5681}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.DicomCast.Core", "src\Microsoft.Health.DicomCast.Core\Microsoft.Health.DicomCast.Core.csproj", "{AAD757E8-D347-4006-BC43-11D761B04078}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.DicomCast.Hosting", "src\Microsoft.Health.DicomCast.Hosting\Microsoft.Health.DicomCast.Hosting.csproj", "{3D4D8CBB-B32F-4BB3-98B9-39F457C71E92}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.DicomCast.Core.UnitTests", "src\Microsoft.Health.DicomCast.Core.UnitTests\Microsoft.Health.DicomCast.Core.UnitTests.csproj", "{E60031C2-46AB-40E3-BDD9-A8F28A13CF32}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Dicom.Client", "..\..\src\Microsoft.Health.Dicom.Client\Microsoft.Health.Dicom.Client.csproj", "{3CCE3438-139A-420A-8A60-1144CADA95A1}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.DicomCast.TableStorage.UnitTests", "src\Microsoft.Health.DicomCast.TableStorage.UnitTests\Microsoft.Health.DicomCast.TableStorage.UnitTests.csproj", "{1444027E-07DA-412E-8E74-F077F5520530}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.DicomCast.TableStorage", "src\Microsoft.Health.DicomCast.TableStorage\Microsoft.Health.DicomCast.TableStorage.csproj", "{0DFB24D5-5E2F-435D-A435-F70CD1824F08}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.FellowOakDicom", "..\..\forks\Microsoft.Health.FellowOakDicom\Microsoft.Health.FellowOakDicom.csproj", "{972888D8-AFAE-4F88-9405-2B66CE70C41F}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|x64 = Debug|x64 - Release|x64 = Release|x64 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {AAD757E8-D347-4006-BC43-11D761B04078}.Debug|x64.ActiveCfg = Debug|x64 - {AAD757E8-D347-4006-BC43-11D761B04078}.Debug|x64.Build.0 = Debug|x64 - {AAD757E8-D347-4006-BC43-11D761B04078}.Release|x64.ActiveCfg = Release|x64 - {AAD757E8-D347-4006-BC43-11D761B04078}.Release|x64.Build.0 = Release|x64 - {3D4D8CBB-B32F-4BB3-98B9-39F457C71E92}.Debug|x64.ActiveCfg = Debug|x64 - {3D4D8CBB-B32F-4BB3-98B9-39F457C71E92}.Debug|x64.Build.0 = Debug|x64 - {3D4D8CBB-B32F-4BB3-98B9-39F457C71E92}.Release|x64.ActiveCfg = Release|x64 - {3D4D8CBB-B32F-4BB3-98B9-39F457C71E92}.Release|x64.Build.0 = Release|x64 - {E60031C2-46AB-40E3-BDD9-A8F28A13CF32}.Debug|x64.ActiveCfg = Debug|x64 - {E60031C2-46AB-40E3-BDD9-A8F28A13CF32}.Debug|x64.Build.0 = Debug|x64 - {E60031C2-46AB-40E3-BDD9-A8F28A13CF32}.Release|x64.ActiveCfg = Release|x64 - {E60031C2-46AB-40E3-BDD9-A8F28A13CF32}.Release|x64.Build.0 = Release|x64 - {3CCE3438-139A-420A-8A60-1144CADA95A1}.Debug|x64.ActiveCfg = Debug|x64 - {3CCE3438-139A-420A-8A60-1144CADA95A1}.Debug|x64.Build.0 = Debug|x64 - {3CCE3438-139A-420A-8A60-1144CADA95A1}.Release|x64.ActiveCfg = Release|x64 - {3CCE3438-139A-420A-8A60-1144CADA95A1}.Release|x64.Build.0 = Release|x64 - {1444027E-07DA-412E-8E74-F077F5520530}.Debug|x64.ActiveCfg = Debug|x64 - {1444027E-07DA-412E-8E74-F077F5520530}.Debug|x64.Build.0 = Debug|x64 - {1444027E-07DA-412E-8E74-F077F5520530}.Release|x64.ActiveCfg = Release|x64 - {1444027E-07DA-412E-8E74-F077F5520530}.Release|x64.Build.0 = Release|x64 - {0DFB24D5-5E2F-435D-A435-F70CD1824F08}.Debug|x64.ActiveCfg = Debug|x64 - {0DFB24D5-5E2F-435D-A435-F70CD1824F08}.Debug|x64.Build.0 = Debug|x64 - {0DFB24D5-5E2F-435D-A435-F70CD1824F08}.Release|x64.ActiveCfg = Release|x64 - {0DFB24D5-5E2F-435D-A435-F70CD1824F08}.Release|x64.Build.0 = Release|x64 - {972888D8-AFAE-4F88-9405-2B66CE70C41F}.Debug|x64.ActiveCfg = Debug|x64 - {972888D8-AFAE-4F88-9405-2B66CE70C41F}.Debug|x64.Build.0 = Debug|x64 - {972888D8-AFAE-4F88-9405-2B66CE70C41F}.Release|x64.ActiveCfg = Release|x64 - {972888D8-AFAE-4F88-9405-2B66CE70C41F}.Release|x64.Build.0 = Release|x64 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {AAD757E8-D347-4006-BC43-11D761B04078} = {E15E21A1-0707-4D98-AEDD-ACC7F59C5681} - {3D4D8CBB-B32F-4BB3-98B9-39F457C71E92} = {E15E21A1-0707-4D98-AEDD-ACC7F59C5681} - {E60031C2-46AB-40E3-BDD9-A8F28A13CF32} = {E15E21A1-0707-4D98-AEDD-ACC7F59C5681} - {3CCE3438-139A-420A-8A60-1144CADA95A1} = {E15E21A1-0707-4D98-AEDD-ACC7F59C5681} - {1444027E-07DA-412E-8E74-F077F5520530} = {E15E21A1-0707-4D98-AEDD-ACC7F59C5681} - {0DFB24D5-5E2F-435D-A435-F70CD1824F08} = {E15E21A1-0707-4D98-AEDD-ACC7F59C5681} - {972888D8-AFAE-4F88-9405-2B66CE70C41F} = {E15E21A1-0707-4D98-AEDD-ACC7F59C5681} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {4EF00533-B2AE-4B1B-B4AF-9799AD9F69E2} - EndGlobalSection -EndGlobal diff --git a/converter/dicom-cast/docs/DicomcastDeploymentTemplate.md b/converter/dicom-cast/docs/DicomcastDeploymentTemplate.md deleted file mode 100644 index 21f3174d30..0000000000 --- a/converter/dicom-cast/docs/DicomcastDeploymentTemplate.md +++ /dev/null @@ -1,340 +0,0 @@ -```json -{ - "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "serviceName": { - "minLength": 3, - "maxLength": 24, - "type": "String", - "metadata": { - "description": "Name of the DICOM Cast service container group." - } - }, - "image": { - "defaultValue": "dicomoss.azurecr.io/dicom-cast", - "type": "String", - "metadata": { - "description": "Container image to deploy. Should be of the form repoName/imagename:tag for images stored in public Docker Hub, or a fully qualified URI for other registries. Images from private registries require additional registry credentials." - } - }, - "storageAccountSku": { - "defaultValue": "Standard_LRS", - "allowedValues": [ - "Standard_LRS", - "Standard_GRS", - "Standard_RAGRS", - "Standard_ZRS", - "Premium_LRS", - "Premium_ZRS", - "Standard_GZRS", - "Standard_RAGZRS" - ], - "type": "String" - }, - "deployApplicationInsights": { - "defaultValue": true, - "type": "Bool", - "metadata": { - "description": "Deploy Application Insights for the DICOM server. Disabled for Microsoft Azure Government (MAG)" - } - }, - "applicationInsightsLocation": { - "defaultValue": "[resourceGroup().location]", - "allowedValues": [ - "southeastasia", - "northeurope", - "westeurope", - "eastus", - "southcentralus", - "westus2" - ], - "type": "String" - }, - "cpuCores": { - "defaultValue": "1.0", - "type": "String", - "metadata": { - "description": "The number of CPU cores to allocate to the container." - } - }, - "memoryInGb": { - "defaultValue": "1.5", - "type": "String", - "metadata": { - "description": "The amount of memory to allocate to the container in gigabytes." - } - }, - "location": { - "defaultValue": "[resourceGroup().location]", - "type": "String", - "metadata": { - "description": "Location for all resources." - } - }, - "restartPolicy": { - "defaultValue": "always", - "allowedValues": [ - "never", - "always", - "onfailure" - ], - "type": "String", - "metadata": { - "description": "The behavior of Azure runtime if container has stopped." - } - }, - "dicomWebEndpoint": { - "type": "String", - "metadata": { - "description": "The endpoint of the DICOM Web server." - } - }, - "fhirEndpoint": { - "type": "String", - "metadata": { - "description": "The endpoint of the FHIR server." - } - }, - "patientSystemId": { - "type": "String", - "metadata": { - "description": "Patient SystemId configured by the user" - } - }, - "isIssuerIdUsed": { - "defaultValue": false, - "type": "Bool", - "metadata": { - "description": "Issuer id or patient system id used based on this boolean value" - } - }, - "enforceValidationOfTagValues": { - "defaultValue": false, - "type": "Bool", - "metadata": { - "description": "Enforce validation of all tag values and do not store to FHIR even if only non-required tags are invalid" - } - }, - "ignoreJsonParsingErrors": { - "defaultValue": false, - "type": "Bool", - "metadata": { - "description": "Ignore json parsing errors for DICOM instances with malformed DICOM json" - } - }, - "additionalEnvironmentVariables": { - "defaultValue": [], - "type": "Array", - "metadata": { - "description": "Array of additional enviornment variables with objects with properties 'name' and 'value'. ex: [{\"name\": \"testName\", \"value\": \"testValue\"}]" - } - }, - "virtualNetworkName": { - "type": "String", - "metadata": { - "description": "Virtual network where ACi will be deployed to" - } - }, - "subnetName": { - "type": "String", - "metadata": { - "description": "Subnet within a virtual network which is delegated to ACI" - } - } - }, - "variables": { - "networkProfileName": "aci-networkProfile", - "interfaceConfigName": "eth0", - "interfaceIpConfig": "ipconfigprofile1", - "isMAG": "[or(contains(resourceGroup().location,'usgov'),contains(resourceGroup().location,'usdod'))]", - "serviceName": "[toLower(parameters('serviceName'))]", - "virtualNetworkName": "[parameters('virtualNetworkName')]", - "subnetName": "[parameters('subnetName')]", - "keyvaultName": "[concat(substring(replace(variables('serviceName'), '-', ''), 0, min(11, length(variables('serviceName')))), uniquestring(resourceGroup().id))]", - "containerGroupResourceId": "[resourceId('Microsoft.ContainerInstance/containerGroups/', variables('serviceName'))]", - "deployAppInsights": "[and(parameters('deployApplicationInsights'),not(variables('isMAG')))]", - "appInsightsName": "[concat('AppInsights-', variables('serviceName'))]", - "storageAccountName": "[concat(substring(replace(variables('serviceName'), '-', ''), 0, min(11, length(variables('serviceName')))), uniquestring(resourceGroup().id))]", - "storageResourceId": "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]", - "keyVaultEndpoint": "[if(variables('isMAG'), concat('https://', variables('keyvaultName'), '.vault.usgovcloudapi.net/'), concat('https://', variables('keyvaultName'), '.vault.azure.net/'))]", - "keyVaultResourceId": "[resourceId('Microsoft.KeyVault/vaults', variables('keyvaultName'))]", - "networkProfileResourceId": "[resourceId('Microsoft.Network/virtualNetworks/subnets', variables('virtualNetworkName'), variables('subnetName'))]", - "environmentVariables": [ - { - "name": "Fhir__Endpoint", - "value": "[parameters('fhirEndpoint')]" - }, - { - "name": "DicomWeb__Endpoint", - "value": "[parameters('dicomWebEndpoint')]" - }, - { - "name": "KeyVault__Endpoint", - "value": "[variables('keyVaultEndpoint')]" - }, - { - "name": "DicomCast__Features__EnforceValidationOfTagValues", - "value": "[parameters('enforceValidationOfTagValues')]" - }, - { - "name": "DicomCast__Features__IgnoreJsonParsingErrors", - "value": "[parameters('ignoreJsonParsingErrors')]" - }, - { - "name": "Patient__PatientSystemId", - "value": "[parameters('patientSystemId')]" - }, - { - "name": "Patient__IsIssuerIdUsed", - "value": "[parameters('isIssuerIdUsed')]" - } - ] - }, - "resources": [ - { - "type": "Microsoft.Network/networkProfiles", - "apiVersion": "2020-11-01", - "name": "[variables('serviceName')]", - "location": "[parameters('location')]", - "properties": { - "containerNetworkInterfaceConfigurations": [ - { - "name": "[variables('interfaceConfigName')]", - "properties": { - "ipConfigurations": [ - { - "name": "[variables('interfaceIpConfig')]", - "properties": { - "subnet": { - "id": "[variables('networkProfileResourceId')]" - } - } - } - ] - } - } - ] - } - }, - { - "type": "Microsoft.ContainerInstance/containerGroups", - "apiVersion": "2018-10-01", - "name": "[variables('serviceName')]", - "location": "[parameters('location')]", - "dependsOn": [ - "[concat('Microsoft.Insights/components/', variables('appInsightsName'))]" - ], - "identity": { - "type": "SystemAssigned" - }, - "properties": { - "containers": [ - { - "name": "[variables('serviceName')]", - "properties": { - "image": "[parameters('image')]", - "resources": { - "requests": { - "cpu": "[parameters('cpuCores')]", - "memoryInGb": "[parameters('memoryInGb')]" - } - }, - "environmentVariables": "[concat(variables('environmentVariables'), parameters('additionalEnvironmentVariables'), array(createObject('name', 'ApplicationInsights__InstrumentationKey', 'value', if(variables('deployAppInsights'), reference(concat('Microsoft.Insights/components/', variables('appInsightsName'))).InstrumentationKey, ''))))]" - } - } - ], - "osType": "Linux", - "networkProfile": { - "id": "[resourceId('Microsoft.Network/networkProfiles', variables('serviceName'))]" - }, - "restartPolicy": "[parameters('restartPolicy')]" - } - }, - { - "type": "Microsoft.Insights/components", - "apiVersion": "2015-05-01", - "name": "[variables('appInsightsName')]", - "location": "[parameters('applicationInsightsLocation')]", - "tags": { - "[concat('hidden-link:', variables('containerGroupResourceId'))]": "Resource", - "displayName": "AppInsightsComponent" - }, - "kind": "web", - "properties": { - "Application_Type": "web", - "ApplicationId": "[variables('serviceName')]" - }, - "condition": "[variables('deployAppInsights')]" - }, - { - "type": "Microsoft.Storage/storageAccounts", - "apiVersion": "2019-04-01", - "name": "[variables('storageAccountName')]", - "location": "[resourceGroup().location]", - "tags": {}, - "sku": { - "name": "[parameters('storageAccountSku')]" - }, - "kind": "StorageV2", - "properties": { - "accessTier": "Hot", - "supportsHttpsTrafficOnly": "true" - } - }, - { - "type": "Microsoft.KeyVault/vaults", - "apiVersion": "2015-06-01", - "name": "[variables('keyvaultName')]", - "location": "[resourceGroup().location]", - "dependsOn": [ - "[variables('containerGroupResourceId')]" - ], - "tags": {}, - "properties": { - "sku": { - "family": "A", - "name": "Standard" - }, - "tenantId": "[reference(variables('containerGroupResourceId'), '2018-10-01', 'Full').Identity.tenantId]", - "accessPolicies": [ - { - "tenantId": "[reference(variables('containerGroupResourceId'), '2018-10-01', 'Full').Identity.tenantId]", - "objectId": "[reference(variables('containerGroupResourceId'), '2018-10-01', 'Full').Identity.principalId]", - "permissions": { - "secrets": [ - "get", - "list", - "set" - ] - } - } - ], - "enabledForDeployment": false - } - }, - { - "type": "Microsoft.KeyVault/vaults/secrets", - "apiVersion": "2015-06-01", - "name": "[concat(variables('keyvaultName'), '/TableStore--ConnectionString')]", - "dependsOn": [ - "[variables('keyVaultResourceId')]", - "[variables('storageResourceId')]" - ], - "properties": { - "contentType": "text/plain", - "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(variables('storageResourceId'), providers('Microsoft.Storage', 'storageAccounts').apiVersions[0]).keys[0].value, ';')]" - } - } - ], - "outputs": { - "containerTenantId": { - "type": "String", - "value": "[reference(variables('containerGroupResourceId'), '2018-10-01', 'Full').Identity.tenantId]" - }, - "containerPrincipalId": { - "type": "String", - "value": "[reference(variables('containerGroupResourceId'), '2018-10-01', 'Full').Identity.principalId]" - } - } -} diff --git a/converter/dicom-cast/docs/authentication.md b/converter/dicom-cast/docs/authentication.md deleted file mode 100644 index 4a4d206313..0000000000 --- a/converter/dicom-cast/docs/authentication.md +++ /dev/null @@ -1,96 +0,0 @@ -# Authentication - -The DICOM cast project supports connecting to both DICOM and FHIR servers that require authentication. Currently there are three types of authentication supported for both servers. The authentication can be configured via the application settings by the appropriate values in the `Authentication` property of the given server. - -## Managed Identity - -This option uses the identity of the deployed DICOM cast instance to communicate with the server. - -```json -{ - "DicomWeb": { - "Endpoint": "https://dicom-server.example.com", - "Authentication": { - "Enabled": true, - "AuthenticationType": "ManagedIdentity", - "ManagedIdentityCredential": { - "Resource": "https://dicom-server.example.com/" - } - } - } -} -``` - -## OAuth2 Client Credential - -This option uses a `client_credentials` OAuth2 grant to obtain an identity to communicate with the server. - -```json -{ - "DicomWeb": { - "Endpoint": "https://dicom-server.example.com", - "Authentication": { - "Enabled": true, - "AuthenticationType": "OAuth2ClientCredential", - "OAuth2ClientCredential": { - "TokenUri": "https://idp.example.com/connect/token", - "Resource": "https://dicom-server.example.com", - "Scope": "https://dicom-server.example.com", - "ClientId": "bdba742b-8138-4b7c-a6d8-03cbb7a8c053", - "ClientSecret": "d8147077-d907-4551-8f40-90c6e86f3f0e" - } - } - } -} -``` - -## OAuth2 User Password - -This option uses a `password` OAuth2 grant to obtain an identity to communicate with the server. - -```json -{ - "DicomWeb": { - "Endpoint": "https://dicom-server.example.com", - "Authentication": { - "Enabled": true, - "AuthenticationType": "OAuth2UserPasswordCredential", - "OAuth2ClientCredential": { - "TokenUri": "https://idp.example.com/connect/token", - "Resource": "https://dicom-server.example.com", - "Scope": "https://dicom-server.example.com", - "ClientId": "bdba742b-8138-4b7c-a6d8-03cbb7a8c053", - "ClientSecret": "d8147077-d907-4551-8f40-90c6e86f3f0e", - "Username": "user@example.com", - "Password": "randomstring" - } - } - } -} -``` - -## Secrets Management - -There are currently two ways provided to store secrets within the application. - -### User-Secrets - -User secrets are enabled when the `EnvironmentName` is `Development`. You can read more about the use of user secrets in [Safe storage of app secrets in development in ASP.NET Core](https://docs.microsoft.com/aspnet/core/security/app-secrets?view=aspnetcore-3.1). - -### KeyVault - -Using KeyVault to store secrets can be enabled by entering a value into the `KeyVault:Endpoint` configuration. On application start this will use the [current identity of the application](https://docs.microsoft.com/en-us/aspnet/core/security/key-vault-configuration?view=aspnetcore-3.1#use-managed-identities-for-azure-resources) to read the key vault and add a configuration provider. - -Below is an example of the settings need to be added to the KeyVault for OAuth2ClientCredential authentication: - -* Add secrets related to Authentication in KeyVault for Medical Imaging Server for DICOM. - + Example: If Medical Imaging Server for Azure was configured with `OAuth2ClientCredential`, below is the list of secrets that need to added to the KeyVault. - - DicomWeb--Authentication--Enabled : True - - DicomWeb--Authentication--AuthenticationType : OAuth2ClientCredential - - DicomWeb--Authentication--OAuth2ClientCredential--TokenUri : `````` - - DicomWeb--Authentication--OAuth2ClientCredential--Resource : ```Application ID URI of the resource app``` - - DicomWeb--Authentication--OAuth2ClientCredential--Scope : ```Application ID URI of the resource app``` - - DicomWeb--Authentication--OAuth2ClientCredential--ClientId : ```Client Id of the client app``` - - DicomWeb--Authentication--OAuth2ClientCredential--ClientSecret : ```Client app secret``` -* Add similar secrets to KeyVault for FHIR™ server. -* Stop and Start the Container, to pickup the new configurations. diff --git a/converter/dicom-cast/docs/image-1.png b/converter/dicom-cast/docs/image-1.png deleted file mode 100644 index c79ecc4284..0000000000 Binary files a/converter/dicom-cast/docs/image-1.png and /dev/null differ diff --git a/converter/dicom-cast/docs/image-2.png b/converter/dicom-cast/docs/image-2.png deleted file mode 100644 index 73be295abd..0000000000 Binary files a/converter/dicom-cast/docs/image-2.png and /dev/null differ diff --git a/converter/dicom-cast/docs/image-3.png b/converter/dicom-cast/docs/image-3.png deleted file mode 100644 index 01d6e9cf3d..0000000000 Binary files a/converter/dicom-cast/docs/image-3.png and /dev/null differ diff --git a/converter/dicom-cast/docs/image.png b/converter/dicom-cast/docs/image.png deleted file mode 100644 index 2dad420f37..0000000000 Binary files a/converter/dicom-cast/docs/image.png and /dev/null differ diff --git a/converter/dicom-cast/docs/workingWithPrivateLink.md b/converter/dicom-cast/docs/workingWithPrivateLink.md deleted file mode 100644 index 1a1cc42dae..0000000000 --- a/converter/dicom-cast/docs/workingWithPrivateLink.md +++ /dev/null @@ -1,26 +0,0 @@ -# Configuration steps for DICOM Cast to work with Private Link enabled DICOM - -1. Create a Virtual Network with two subnets within the same subscription and region as you would plan to create the Health Data Services Workspace. - 1. Default subnet - 2. Subnet delegated to Microsoft.containerinstance/containergroups -![Alt text](image.png) - -2. Provision Health Data Services workspace, DICOM and FHIR in the same region. -3. Enable Private Link to Health Data Service Workspace. This Private Link would use the Virtual Network created in step 1 and default subnet -![Alt text](image-1.png) - -4. Use the template given [here](DicomcastDeploymentTemplate.md) to deploy DICOM Cast within a Virtual Network created in step 1. This will use the subnet that is delegated to Microsoft.containerinstance/containergroups as shown in picture in step 1. -5. Add the following role assignments to Health Data Service Workspace on container instances System Assigned Managed Identity. - 1. DICOM Data Owner - 2. FHIR Data Contributor -![Alt text](image-2.png) - -6. DICOM Cast needs to talk to table storage within the storage account for processing Change Feed. Enable Private Link to the storage account created as a part of template deployment in step 4. This Private Link should also be in same vnet and default subnet that is created in step 1. -![Alt text](image-3.png) - -7. Disable public network access for the storage account (`Security + Networking` > `Networking` > `Firewalls and virtual networks` > `Public Network Acccess` > `Disabled`) - -8. Ensure all the fields mentioned here are populated in the key vault provisioned in step 4. https://github.com/microsoft/dicom-server/blob/main/docs/how-to-guides/sync-dicom-metadata-to-fhir.md#update-key-vault-for-dicom-cast - -9. Restart DICOM Cast container. It should be running successfully. - diff --git a/converter/dicom-cast/samples/templates/default-azuredeploy.json b/converter/dicom-cast/samples/templates/default-azuredeploy.json deleted file mode 100644 index 330e402645..0000000000 --- a/converter/dicom-cast/samples/templates/default-azuredeploy.json +++ /dev/null @@ -1,298 +0,0 @@ -{ - "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "serviceName": { - "minLength": 3, - "maxLength": 24, - "type": "String", - "metadata": { - "description": "Name of the DICOM Cast service container group." - } - }, - "image": { - "defaultValue": "dicomoss.azurecr.io/dicom-cast", - "type": "String", - "metadata": { - "description": "Container image to deploy. Should be of the form repoName/imagename for images stored in public Docker Hub, or a fully qualified URI for other registries. Images from private registries require additional registry credentials." - } - }, - "imageTag": { - "type": "String", - "metadata": { - "description": "Image tag. Ex: 10.0.479. You can find the latest https://github.com/microsoft/dicom-server/tags" - } - }, - "storageAccountSku": { - "defaultValue": "Standard_LRS", - "allowedValues": [ - "Standard_LRS", - "Standard_GRS", - "Standard_RAGRS", - "Standard_ZRS", - "Premium_LRS", - "Premium_ZRS", - "Standard_GZRS", - "Standard_RAGZRS" - ], - "type": "String" - }, - "deployApplicationInsights": { - "defaultValue": true, - "type": "Bool", - "metadata": { - "description": "Deploy Application Insights for the DICOM server. Disabled for Microsoft Azure Government (MAG)" - } - }, - "applicationInsightsLocation": { - "defaultValue": "[resourceGroup().location]", - "allowedValues": [ - "southeastasia", - "northeurope", - "westeurope", - "eastus", - "southcentralus", - "westus2" - ], - "type": "String" - }, - "cpuCores": { - "defaultValue": "1.0", - "type": "String", - "metadata": { - "description": "The number of CPU cores to allocate to the container." - } - }, - "memoryInGb": { - "defaultValue": "1.5", - "type": "String", - "metadata": { - "description": "The amount of memory to allocate to the container in gigabytes." - } - }, - "location": { - "defaultValue": "[resourceGroup().location]", - "type": "String", - "metadata": { - "description": "Location for all resources." - } - }, - "restartPolicy": { - "defaultValue": "always", - "allowedValues": [ - "never", - "always", - "onfailure" - ], - "type": "String", - "metadata": { - "description": "The behavior of Azure runtime if container has stopped." - } - }, - "dicomWebEndpoint": { - "type": "string", - "metadata": { - "description": "The endpoint of the DICOM Web server." - } - }, - "fhirEndpoint": { - "type": "string", - "metadata": { - "description": "The endpoint of the FHIR server." - } - }, - "patientSystemId": { - "type": "string", - "metadata": { - "description": "Patient SystemId configured by the user" - } - }, - "isIssuerIdUsed": { - "defaultValue": false, - "type": "Bool", - "metadata": { - "description": "Issuer id or patient system id used based on this boolean value" - } - }, - "enforceValidationOfTagValues": { - "defaultValue": false, - "type": "Bool", - "metadata": { - "description": "Enforce validation of all tag values and do not store to FHIR even if only non-required tags are invalid" - } - }, - "ignoreJsonParsingErrors": { - "defaultValue": false, - "type": "Bool", - "metadata": { - "description": "Ignore json parsing errors for DICOM instances with malformed DICOM json" - } - }, - "additionalEnvironmentVariables": { - "defaultValue": [], - "type": "array", - "metadata": { - "description": "Array of additional enviornment variables with objects with properties 'name' and 'value'. ex: [{\"name\": \"testName\", \"value\": \"testValue\"}]" - } - } - }, - "variables": { - "isMAG": "[or(contains(resourceGroup().location,'usgov'),contains(resourceGroup().location,'usdod'))]", - "serviceName": "[toLower(parameters('serviceName'))]", - "containerGroupResourceId": "[resourceId('Microsoft.ContainerInstance/containerGroups/', variables('serviceName'))]", - "deployAppInsights": "[and(parameters('deployApplicationInsights'),not(variables('isMAG')))]", - "appInsightsName": "[concat('AppInsights-', variables('serviceName'))]", - "storageAccountName": "[concat(substring(replace(variables('serviceName'), '-', ''), 0, min(11, length(variables('serviceName')))), uniquestring(resourceGroup().id))]", - "storageResourceId": "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]", - "keyVaultEndpoint": "[if(variables('isMAG'), concat('https://', variables('serviceName'), '.vault.usgovcloudapi.net/'), concat('https://', variables('serviceName'), '.vault.azure.net/'))]", - "keyVaultResourceId": "[resourceId('Microsoft.KeyVault/vaults', variables('serviceName'))]", - "environmentVariables": [ - { - "name": "Fhir__Endpoint", - "value": "[parameters('fhirEndpoint')]" - }, - { - "name": "DicomWeb__Endpoint", - "value": "[parameters('dicomWebEndpoint')]" - }, - { - "name": "KeyVault__Endpoint", - "value": "[variables('keyVaultEndpoint')]" - }, - { - "name": "DicomCast__Features__EnforceValidationOfTagValues", - "value": "[parameters('enforceValidationOfTagValues')]" - }, - { - "name": "DicomCast__Features__IgnoreJsonParsingErrors", - "value": "[parameters('ignoreJsonParsingErrors')]" - }, - { - "name": "Patient__PatientSystemId", - "value": "[parameters('patientSystemId')]" - }, - { - "name": "Patient__IsIssuerIdUsed", - "value": "[parameters('isIssuerIdUsed')]" - } - ] - }, - "resources": [ - { - "type": "Microsoft.ContainerInstance/containerGroups", - "apiVersion": "2018-10-01", - "name": "[variables('serviceName')]", - "location": "[parameters('location')]", - "dependsOn": [ - "[concat('Microsoft.Insights/components/', variables('appInsightsName'))]" - ], - "identity": { - "type": "SystemAssigned" - }, - "properties": { - "containers": [ - { - "name": "[variables('serviceName')]", - "properties": { - "image": "[concat(parameters('image'), ':', parameters('imageTag'))]", - "resources": { - "requests": { - "cpu": "[parameters('cpuCores')]", - "memoryInGb": "[parameters('memoryInGb')]" - } - }, - "environmentVariables": "[concat(variables('environmentVariables'), parameters('additionalEnvironmentVariables'), array(createObject('name', 'ApplicationInsights__InstrumentationKey', 'value', if(variables('deployAppInsights'), reference(concat('Microsoft.Insights/components/', variables('appInsightsName'))).InstrumentationKey, ''))))]" - } - } - ], - "osType": "Linux", - "restartPolicy": "[parameters('restartPolicy')]" - } - }, - { - "type": "Microsoft.Insights/components", - "apiVersion": "2015-05-01", - "name": "[variables('appInsightsName')]", - "location": "[parameters('applicationInsightsLocation')]", - "tags": { - "[concat('hidden-link:', variables('containerGroupResourceId'))]": "Resource", - "displayName": "AppInsightsComponent" - }, - "kind": "web", - "properties": { - "Application_Type": "web", - "ApplicationId": "[variables('serviceName')]" - }, - "condition": "[variables('deployAppInsights')]" - }, - { - "type": "Microsoft.Storage/storageAccounts", - "apiVersion": "2019-04-01", - "name": "[variables('storageAccountName')]", - "location": "[resourceGroup().location]", - "tags": {}, - "sku": { - "name": "[parameters('storageAccountSku')]" - }, - "kind": "StorageV2", - "properties": { - "accessTier": "Hot", - "supportsHttpsTrafficOnly": "true" - } - }, - { - "type": "Microsoft.KeyVault/vaults", - "apiVersion": "2015-06-01", - "name": "[variables('serviceName')]", - "location": "[resourceGroup().location]", - "dependsOn": [ - "[variables('containerGroupResourceId')]" - ], - "tags": {}, - "properties": { - "sku": { - "family": "A", - "name": "Standard" - }, - "tenantId": "[reference(variables('containerGroupResourceId'), '2018-10-01', 'Full').Identity.tenantId]", - "accessPolicies": [ - { - "tenantId": "[reference(variables('containerGroupResourceId'), '2018-10-01', 'Full').Identity.tenantId]", - "objectId": "[reference(variables('containerGroupResourceId'), '2018-10-01', 'Full').Identity.principalId]", - "permissions": { - "secrets": [ - "get", - "list", - "set" - ] - } - } - ], - "enabledForDeployment": false - } - }, - { - "type": "Microsoft.KeyVault/vaults/secrets", - "apiVersion": "2015-06-01", - "name": "[concat(variables('serviceName'), '/TableStore--ConnectionString')]", - "dependsOn": [ - "[variables('keyVaultResourceId')]", - "[variables('storageResourceId')]" - ], - "properties": { - "contentType": "text/plain", - "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(variables('storageResourceId'), providers('Microsoft.Storage', 'storageAccounts').apiVersions[0]).keys[0].value, ';')]" - } - } - ], - "outputs": { - "containerTenantId": { - "type": "string", - "value": "[reference(variables('containerGroupResourceId'), '2018-10-01', 'Full').Identity.tenantId]" - }, - "containerPrincipalId": { - "type": "string", - "value": "[reference(variables('containerGroupResourceId'), '2018-10-01', 'Full').Identity.principalId]" - } - } -} diff --git a/converter/dicom-cast/samples/templates/deploy-with-healthcareapis.json b/converter/dicom-cast/samples/templates/deploy-with-healthcareapis.json deleted file mode 100644 index 0686c9349c..0000000000 --- a/converter/dicom-cast/samples/templates/deploy-with-healthcareapis.json +++ /dev/null @@ -1,319 +0,0 @@ -{ - "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "workspaceRegion": { - "defaultValue": "[resourceGroup().location", - "type": "string", - "allowedValues": [ - "australiaeast", - "canadacentral", - "eastus", - "eastus2", - "germanywestcentral", - "japaneast", - "northcentralus", - "northeurope", - "southafricanorth", - "southcentralus", - "southeastasia", - "switzerlandnorth", - "uksouth", - "ukwest", - "westcentralus", - "westeurope", - "westus2" - ] - }, - "workspaceName": { - "type": "string", - "metadata": { - "description": "Name of the workspace." - } - }, - "fhirServiceName": { - "type": "string", - "metadata": { - "description": "Name of the workspace FHIR service." - } - }, - "dicomServiceName": { - "type": "string", - "metadata": { - "description": "Name of the workspace DICOM service." - } - }, - "serviceName": { - "minLength": 3, - "maxLength": 24, - "type": "string", - "metadata": { - "description": "Name of the DICOM Cast service container group." - } - }, - "image": { - "defaultValue": "dicomoss.azurecr.io/dicom-cast", - "type": "string", - "metadata": { - "description": "Container image to deploy. Should be of the form repoName/imagename for images stored in public Docker Hub, or a fully qualified URI for other registries. Images from private registries require additional registry credentials." - } - }, - "imageTag": { - "type": "string", - "metadata": { - "description": "Image tag. Ex: 10.0.479. You can find the latest https://github.com/microsoft/dicom-server/tags" - } - }, - "storageAccountSku": { - "defaultValue": "Standard_LRS", - "allowedValues": [ - "Standard_LRS", - "Standard_GRS", - "Standard_RAGRS", - "Standard_ZRS", - "Premium_LRS", - "Premium_ZRS", - "Standard_GZRS", - "Standard_RAGZRS" - ], - "type": "string" - }, - "deployApplicationInsights": { - "defaultValue": true, - "type": "bool", - "metadata": { - "description": "Deploy Application Insights for the DICOM server. Disabled for Microsoft Azure Government (MAG)" - } - }, - "applicationInsightsLocation": { - "defaultValue": "[resourceGroup().location]", - "allowedValues": [ - "southeastasia", - "northeurope", - "westeurope", - "eastus", - "southcentralus", - "westus2" - ], - "type": "string" - }, - "cpuCores": { - "defaultValue": "1.0", - "type": "string", - "metadata": { - "description": "The number of CPU cores to allocate to the container." - } - }, - "memoryInGb": { - "defaultValue": "1.5", - "type": "string", - "metadata": { - "description": "The amount of memory to allocate to the container in gigabytes." - } - }, - "location": { - "defaultValue": "[resourceGroup().location]", - "type": "string", - "metadata": { - "description": "Location for all resources." - } - }, - "restartPolicy": { - "defaultValue": "always", - "allowedValues": [ - "never", - "always", - "onfailure" - ], - "type": "string", - "metadata": { - "description": "The behavior of Azure runtime if container has stopped." - } - }, - "patientSystemId": { - "defaultValue": "", - "type": "string", - "metadata": { - "description": "Patient SystemId configured by the user" - } - }, - "isIssuerIdUsed": { - "defaultValue": false, - "type": "bool", - "metadata": { - "description": "Issuer id or patient system id used based on this boolean value" - } - }, - "enforceValidationOfTagValues": { - "defaultValue": false, - "type": "bool", - "metadata": { - "description": "Enforce validation of all tag values and do not store to FHIR even if only non-required tags are invalid" - } - }, - "ignoreJsonParsingErrors": { - "defaultValue": false, - "type": "Bool", - "metadata": { - "description": "Ignore json parsing errors for DICOM instances with malformed DICOM json" - } - } - }, - "variables": { - "authority": "[concat('https://login.microsoftonline.com/', subscription().tenantId)]", - "fhirServiceName": "[concat(parameters('workspaceName'), '/', parameters('fhirServiceName'))]", - "dicomServiceName": "[concat(parameters('workspaceName'), '/', parameters('dicomServiceName'))]", - "fhirEndpoint": "[concat('https://', parameters('workspaceName'), '-', parameters('fhirServiceName'), '.fhir.azurehealthcareapis.com')]", - "dicomWebEndpoint": "[concat('https://', parameters('workspaceName'), '-', parameters('dicomServiceName'), '.dicom.azurehealthcareapis.com')]", - "authenticationArray": [ - { - "name": "DicomWeb__Authentication__AuthenticationType", - "value": "ManagedIdentity" - }, - { - "name": "DicomWeb__Authentication__Enabled", - "value": "true" - }, - { - "name": "DicomWeb__Authentication__ManagedIdentityCredential__Resource", - "value": "https://dicom.healthcareapis.azure.com" - }, - { - "name": "Fhir__Authentication__AuthenticationType", - "value": "ManagedIdentity" - }, - { - "name": "Fhir__Authentication__Enabled", - "value": "true" - }, - { - "name": "Fhir__Authentication__ManagedIdentityCredential__Resource", - "value": "[variables('fhirEndpoint')]" - } - ] - }, - "resources": [{ - "type": "Microsoft.HealthcareApis/workspaces", - "name": "[parameters('workspaceName')]", - "apiVersion": "2021-11-01", - "location": "[parameters('workspaceRegion')]", - "properties": {} - }, - { - "type": "Microsoft.HealthcareApis/workspaces/fhirservices", - "kind": "fhir-R4", - "name": "[variables('fhirServiceName')]", - "apiVersion": "2021-11-01", - "location": "[parameters('workspaceRegion')]", - "dependsOn": [ - "[resourceId('Microsoft.HealthcareApis/workspaces', parameters('workspaceName'))]" - ], - "properties": { - "authenticationConfiguration": { - "authority": "[variables('authority')]", - "audience": "[variables('fhirEndpoint')]" - } - } - }, - { - "type": "Microsoft.HealthcareApis/workspaces/dicomservices", - "name": "[variables('dicomServiceName')]", - "apiVersion": "2021-11-01", - "location": "[parameters('workspaceRegion')]", - "dependsOn": [ - "[resourceId('Microsoft.HealthcareApis/workspaces', parameters('workspaceName'))]" - ], - "properties": {} - }, - { - "type": "Microsoft.Resources/deployments", - "apiVersion": "2021-04-01", - "name": "linkedTemplate", - "dependsOn": [ - "[resourceId('Microsoft.HealthcareApis/workspaces/fhirservices', parameters('workspaceName'), parameters('fhirServiceName'))]", - "[resourceId('Microsoft.HealthcareApis/workspaces/dicomservices', parameters('workspaceName'), parameters('dicomServiceName'))]" - ], - "properties": { - "mode": "Incremental", - "templateLink": { - "uri": "https://raw.githubusercontent.com/microsoft/dicom-server/main/converter/dicom-cast/samples/templates/default-azuredeploy.json", - "contentVersion": "1.0.0.0" - }, - "parameters": { - "serviceName": { - "value": "[parameters('serviceName')]" - }, - "imageTag": { - "value": "[parameters('imageTag')]" - }, - "image": { - "value": "[parameters('image')]" - }, - "storageAccountSku": { - "value": "[parameters('storageAccountSku')]" - }, - "deployApplicationInsights": { - "value": "[parameters('deployApplicationInsights')]" - }, - "applicationInsightsLocation": { - "value": "[parameters('applicationInsightsLocation')]" - }, - "cpuCores": { - "value": "[parameters('cpuCores')]" - }, - "memoryInGb": { - "value": "[parameters('memoryInGb')]" - }, - "location": { - "value": "[parameters('location')]" - }, - "dicomWebEndpoint": { - "value": "[variables('dicomWebEndpoint')]" - }, - "fhirEndpoint": { - "value": "[variables('fhirEndpoint')]" - }, - "enforceValidationOfTagValues": { - "value": "[parameters('enforceValidationOfTagValues')]" - }, - "ignoreJsonParsingErrors": { - "value": "[parameters('ignoreJsonParsingErrors')]" - }, - "additionalEnvironmentVariables": { - "value": "[variables('authenticationArray')]" - }, - "patientSystemId": { - "value": "[parameters('patientSystemId')]" - }, - "isIssuerIdUsed": { - "value": "[parameters('isIssuerIdUsed')]" - } - } - } - }, - { - "type": "Microsoft.HealthcareApis/workspaces/providers/roleAssignments", - "apiVersion": "2021-04-01-preview", - "name": "[concat(parameters('workspaceName'), '/Microsoft.Authorization/', guid(parameters('fhirServiceName')))]", - "dependsOn": [ - "[resourceId('Microsoft.Resources/deployments', 'linkedTemplate')]" - ], - "properties": { - "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions/', '5a1fc7df-4bf1-4951-a576-89034ee01acd')]", - "principalId": "[reference('linkedTemplate').outputs.containerPrincipalId.value]" - } - }, - { - "type": "Microsoft.HealthcareApis/workspaces/providers/roleAssignments", - "apiVersion": "2021-04-01-preview", - "name": "[concat(parameters('workspaceName'), '/Microsoft.Authorization/', guid(parameters('dicomServiceName')))]", - "dependsOn": [ - "[resourceId('Microsoft.Resources/deployments', 'linkedTemplate')]" - ], - "properties": { - "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions/', '58a3b984-7adf-4c20-983a-32417c86fbc8')]", - "principalId": "[reference('linkedTemplate').outputs.containerPrincipalId.value]" - } - } - ] -} \ No newline at end of file diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/ChangeFeedGenerator.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/ChangeFeedGenerator.cs deleted file mode 100644 index aea8f6c74f..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/ChangeFeedGenerator.cs +++ /dev/null @@ -1,56 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using FellowOakDicom; -using Microsoft.Health.Dicom.Client.Models; - -namespace Microsoft.Health.DicomCast.Core.UnitTests; - -public static class ChangeFeedGenerator -{ - public static ChangeFeedEntry Generate(long? sequence = null, ChangeFeedAction? action = null, string studyInstanceUid = null, string seriesInstanceUid = null, string sopInstanceUid = null, ChangeFeedState? state = null, DicomDataset metadata = null) - { - if (sequence == null) - { - sequence = 1; - } - - if (action == null) - { - action = ChangeFeedAction.Create; - } - - if (string.IsNullOrEmpty(studyInstanceUid)) - { - studyInstanceUid = DicomUID.Generate().UID; - } - - if (string.IsNullOrEmpty(seriesInstanceUid)) - { - seriesInstanceUid = DicomUID.Generate().UID; - } - - if (string.IsNullOrEmpty(sopInstanceUid)) - { - sopInstanceUid = DicomUID.Generate().UID; - } - - if (state == null) - { - state = ChangeFeedState.Current; - } - - return new ChangeFeedEntry( - sequence.Value, - DateTimeOffset.UtcNow, - action.Value, - studyInstanceUid, - seriesInstanceUid, - sopInstanceUid, - state.Value, - metadata: metadata); - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Fhir/FhirResourceValidatorTests.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Fhir/FhirResourceValidatorTests.cs deleted file mode 100644 index 610121edfa..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Fhir/FhirResourceValidatorTests.cs +++ /dev/null @@ -1,62 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Hl7.Fhir.Model; -using Microsoft.Health.DicomCast.Core.Features.Fhir; -using Xunit; - -namespace Microsoft.Health.DicomCast.Core.UnitTests.Features.Fhir; - -public class FhirResourceValidatorTests -{ - private readonly FhirResourceValidator _fhirResourceValidator = new FhirResourceValidator(); - - [Fact] - public void GivenAResourceMissingId_WhenValidated_ThenInvalidFhirResourceExceptionShouldBeThrown() - { - var patient = new Patient(); - - Assert.Throws(() => _fhirResourceValidator.Validate(patient)); - } - - [Fact] - public void GivenAResourceMissingMeta_WhenValidated_ThenInvalidFhirResourceExceptionShouldBeThrown() - { - var patient = new Patient() - { - Id = "p1", - Meta = null, - }; - - Assert.Throws(() => _fhirResourceValidator.Validate(patient)); - } - - [Fact] - public void GivenAResourceMissingVersionId_WhenValidated_ThenInvalidFhirResourceExceptionShouldBeThrown() - { - var patient = new Patient() - { - Id = "p1", - Meta = new Meta(), - }; - - Assert.Throws(() => _fhirResourceValidator.Validate(patient)); - } - - [Fact] - public void GivenAValidResource_WhenValidated_ThenItShouldNotThrowException() - { - var patient = new Patient() - { - Id = "p1", - Meta = new Meta() - { - VersionId = "1", - }, - }; - - _fhirResourceValidator.Validate(patient); - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Fhir/FhirServiceTests.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Fhir/FhirServiceTests.cs deleted file mode 100644 index 219b579f06..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Fhir/FhirServiceTests.cs +++ /dev/null @@ -1,277 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Hl7.Fhir.Model; -using Microsoft.Health.DicomCast.Core.Extensions; -using Microsoft.Health.DicomCast.Core.Features.Fhir; -using Microsoft.Health.Fhir.Client; -using NSubstitute; -using Xunit; -using static Hl7.Fhir.Model.CapabilityStatement; -using Task = System.Threading.Tasks.Task; - -namespace Microsoft.Health.DicomCast.Core.UnitTests.Features.Fhir; - -public class FhirServiceTests -{ - private static readonly CancellationToken DefaultCancellationToken = new CancellationTokenSource().Token; - private static readonly Identifier DefaultPatientIdentifier = new Identifier(string.Empty, "p1"); - private static readonly Identifier DefaultImagingStudyIdentifier = new Identifier(string.Empty, "123"); - private const string MetaDataEndpoint = "metadata"; - - private readonly IFhirClient _fhirClient = Substitute.For(); - private readonly IFhirResourceValidator _fhirResourceValidator = Substitute.For(); - private readonly FhirService _fhirService; - - public FhirServiceTests() - { - _fhirService = new FhirService(_fhirClient, _fhirResourceValidator); - } - - private delegate Task RetrieveAsyncDelegate(Identifier identifier, CancellationToken cancellationToken); - - [Fact] - public async Task GivenNoMatchingResource_WhenPatientIsRetrieved_ThenItShouldNotBeValidated() - { - await ExecuteAndValidateNoMatch(DefaultPatientIdentifier, _fhirService.RetrievePatientAsync); - } - - [Fact] - public async Task GivenASingleMatch_WhenPatientIsRetrieved_ThenItShouldBeValidated() - { - await ExecuteAndValidateSingleMatchAsync(DefaultPatientIdentifier, _fhirService.RetrievePatientAsync); - } - - [Fact] - public async Task GivenASingleMatchNotInFirstResultSet_WhenPatientIsRetrieved_ThenCorrectPatientShouldBeReturned() - { - await ExecuteAndValidateSingleMatchNotInFirstResultSetAsync(DefaultPatientIdentifier, _fhirService.RetrievePatientAsync); - } - - [Fact] - public async Task GivenMultipleMatches_WhenPatientIsRetrieved_ThenMultipleMatchingResourcesExceptionShouldBeThrown() - { - await ExecuteAndValidateMultipleMatchesAsync(_fhirService.RetrievePatientAsync); - } - - [Fact] - public async Task GivenMultipleMatchesThatSpansMultipleResultSet_WhenPatientIsRetrieved_ThenMultipleMatchingResourcesExceptionShouldBeThrown() - { - await ExecuteAndValidateMultipleMatchesThatSpansInMultipleResultSetAsync(DefaultImagingStudyIdentifier, _fhirService.RetrievePatientAsync); - } - - [Fact] - public async Task GivenNoMatchingResource_WhenImagingStudyIsRetrieved_ThenItShouldNotBeValidated() - { - await ExecuteAndValidateNoMatch(DefaultPatientIdentifier, _fhirService.RetrieveImagingStudyAsync); - } - - [Fact] - public async Task GivenASingleMatch_WhenImagingStudyIsRetrieved_ThenItShouldBeValidated() - { - await ExecuteAndValidateSingleMatchAsync(DefaultImagingStudyIdentifier, _fhirService.RetrieveImagingStudyAsync); - } - - [Fact] - public async Task GivenASingleMatchNotInFirstResultSet_WhenImagingStudyIsRetrieved_ThenCorrectImagingStudyShouldBeReturned() - { - await ExecuteAndValidateSingleMatchNotInFirstResultSetAsync(DefaultImagingStudyIdentifier, _fhirService.RetrieveImagingStudyAsync); - } - - [Fact] - public async Task GivenMultipleMatches_WhenImagingStudyIsRetrieved_ThenMultipleMatchingResourcesExceptionShouldBeThrown() - { - await ExecuteAndValidateMultipleMatchesAsync(_fhirService.RetrieveImagingStudyAsync); - } - - [Fact] - public async Task GivenMultipleMatchesThatSpansMultipleResultSet_WhenImagingStudyIsRetrieved_ThenMultipleMatchingResourcesExceptionShouldBeThrown() - { - await ExecuteAndValidateMultipleMatchesThatSpansInMultipleResultSetAsync(DefaultImagingStudyIdentifier, _fhirService.RetrieveImagingStudyAsync); - } - - [Fact] - public async Task GivenInValidFhirConfigVersion_ShouldThrowError() - { - _fhirClient.ReadAsync(MetaDataEndpoint, DefaultCancellationToken).Returns(GenerateFhirCapabilityResponse(FHIRVersion.N0_01, SystemRestfulInteraction.Transaction)); - await Assert.ThrowsAsync(() => _fhirService.CheckFhirServiceCapability(DefaultCancellationToken)); - } - - [Fact] - public async Task GivenInValidFhirConfigInteraction_ShouldThrowError() - { - _fhirClient.ReadAsync(MetaDataEndpoint, DefaultCancellationToken).Returns(GenerateFhirCapabilityResponse(FHIRVersion.N4_0_0, SystemRestfulInteraction.Batch)); - await Assert.ThrowsAsync(() => _fhirService.CheckFhirServiceCapability(DefaultCancellationToken)); - } - - [Fact] - public async Task GivenValidFhirConfigV4_ShouldNotThrowError() - { - _fhirClient.ReadAsync(MetaDataEndpoint, DefaultCancellationToken).Returns(GenerateFhirCapabilityResponse(FHIRVersion.N4_0_0, SystemRestfulInteraction.Transaction)); - await _fhirService.CheckFhirServiceCapability(DefaultCancellationToken); - } - - [Fact] - public async Task GivenValidFhirConfigV401_ShouldNotThrowError() - { - _fhirClient.ReadAsync(MetaDataEndpoint, DefaultCancellationToken).Returns(GenerateFhirCapabilityResponse(FHIRVersion.N4_0_1, SystemRestfulInteraction.Transaction)); - await _fhirService.CheckFhirServiceCapability(DefaultCancellationToken); - } - - private void SetupIdentifierSearchCriteria(string typeName, Identifier identifier, Bundle bundle) - { - ResourceType? resourceType = ModelInfo.FhirTypeNameToResourceType(typeName); - Assert.NotNull(resourceType); - - _fhirClient.SearchAsync( - resourceType.Value, - identifier.ToSearchQueryParameter(), - count: null, - DefaultCancellationToken) - .Returns(GenerateFhirResponse(bundle)); - } - - private async Task ExecuteAndValidateNoMatch(Identifier identifier, RetrieveAsyncDelegate retrieve) - where TResource : Resource, new() - { - SetupIdentifierSearchCriteria(new TResource().TypeName, identifier, new Bundle()); - - TResource resource = await retrieve(identifier, DefaultCancellationToken); - - Assert.Null(resource); - _fhirResourceValidator.DidNotReceiveWithAnyArgs().Validate(default); - } - - private async Task ExecuteAndValidateSingleMatchAsync(Identifier identifier, RetrieveAsyncDelegate retrieve) - where TResource : Resource, new() - { - var expectedResource = new TResource(); - - var bundle = new Bundle(); - - bundle.Entry.Add( - new Bundle.EntryComponent() - { - Resource = expectedResource, - }); - - SetupIdentifierSearchCriteria(expectedResource.TypeName, identifier, bundle); - - TResource actualResource = await retrieve(identifier, DefaultCancellationToken); - - Assert.Same(expectedResource, actualResource); - _fhirResourceValidator.Received(1).Validate(expectedResource); - } - - private async Task ExecuteAndValidateSingleMatchNotInFirstResultSetAsync(Identifier identifier, RetrieveAsyncDelegate retrieve) - where TResource : Resource, new() - { - var expectedResource = new TResource(); - Assert.True(expectedResource.TryDeriveResourceType(out ResourceType resourceType)); - - var firstBundle = new Bundle() - { - NextLink = new Uri("next", UriKind.Relative), - }; - - _fhirClient.SearchAsync( - resourceType, - query: Arg.Any(), - count: Arg.Any(), - cancellationToken: DefaultCancellationToken) - .Returns(GenerateFhirResponse(firstBundle)); - - var bundle = new Bundle(); - - bundle.Entry.Add( - new Bundle.EntryComponent() - { - Resource = expectedResource, - }); - - _fhirClient.SearchAsync(url: Arg.Any(), DefaultCancellationToken).Returns(GenerateFhirResponse(bundle)); - - TResource actualResource = await retrieve(identifier, DefaultCancellationToken); - - Assert.Same(expectedResource, actualResource); - } - - private async Task ExecuteAndValidateMultipleMatchesAsync(RetrieveAsyncDelegate retrieve) - where TResource : Resource, new() - { - var expectedResource = new TResource(); - Assert.True(expectedResource.TryDeriveResourceType(out ResourceType resourceType)); - - var bundleEntry = new Bundle.EntryComponent() - { - Resource = expectedResource, - }; - - var bundle = new Bundle(); - - bundle.Entry.AddRange(new[] { bundleEntry, bundleEntry }); - - _fhirClient.SearchAsync( - resourceType, - query: Arg.Any(), - count: Arg.Any(), - cancellationToken: DefaultCancellationToken) - .Returns(GenerateFhirResponse(bundle)); - - await Assert.ThrowsAsync(() => retrieve(new Identifier(), DefaultCancellationToken)); - } - - private async Task ExecuteAndValidateMultipleMatchesThatSpansInMultipleResultSetAsync(Identifier identifier, RetrieveAsyncDelegate retrieve) - where TResource : Resource, new() - { - var expectedResource = new TResource(); - - var firstBundle = new Bundle() - { - NextLink = new Uri("next", UriKind.Relative), - }; - - var bundleEntry = new Bundle.EntryComponent() - { - Resource = expectedResource, - }; - - firstBundle.Entry.Add(bundleEntry); - - SetupIdentifierSearchCriteria(expectedResource.TypeName, identifier, firstBundle); - - var secondBundle = new Bundle(); - - secondBundle.Entry.Add(bundleEntry); - - _fhirClient.SearchAsync(url: Arg.Any(), DefaultCancellationToken).Returns(GenerateFhirResponse(secondBundle)); - - await Assert.ThrowsAsync(() => retrieve(identifier, DefaultCancellationToken)); - } - - private static FhirResponse GenerateFhirResponse(Bundle firstBundle) - { - return new FhirResponse(new HttpResponseMessage(), firstBundle); - } - - private static FhirResponse GenerateFhirCapabilityResponse(FHIRVersion version, SystemRestfulInteraction interaction) - { - var statement = new CapabilityStatement { FhirVersion = version }; - statement.Rest.Add(new RestComponent - { - Interaction = new List - { - new SystemInteractionComponent { Code = interaction }, - }, - }); - - return new FhirResponse(new HttpResponseMessage(), statement); - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Fhir/FhirTransactionExecutorTests.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Fhir/FhirTransactionExecutorTests.cs deleted file mode 100644 index 63b99a42c7..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Fhir/FhirTransactionExecutorTests.cs +++ /dev/null @@ -1,205 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using Hl7.Fhir.Model; -using Hl7.Fhir.Utility; -using Microsoft.Health.DicomCast.Core.Exceptions; -using Microsoft.Health.DicomCast.Core.Features.Fhir; -using Microsoft.Health.Fhir.Client; -using NSubstitute; -using NSubstitute.ExceptionExtensions; -using Xunit; -using Task = System.Threading.Tasks.Task; - -namespace Microsoft.Health.DicomCast.Core.UnitTests.Features.Fhir; - -public class FhirTransactionExecutorTests -{ - private readonly IFhirClient _fhirClient = Substitute.For(); - private readonly FhirTransactionExecutor _fhirTransactionExecutor; - - private readonly Bundle _defaultRequestBundle = new Bundle(); - - public FhirTransactionExecutorTests() - { - _fhirTransactionExecutor = new FhirTransactionExecutor(_fhirClient); - } - - [Fact] - public async Task GivenRequestFailsWithPreconditionFailed_WhenExecuting_ThenResourceConflictExceptionShouldBeThrown() - { - SetupPostException(HttpStatusCode.PreconditionFailed); - - await Assert.ThrowsAsync(() => _fhirTransactionExecutor.ExecuteTransactionAsync(new Bundle(), default)); - } - - [Fact] - public async Task GivenRequestFailsWithTooManyRequests_WhenExecuting_ThenServerTooBusyExceptionShouldBeThrown() - { - SetupPostException(HttpStatusCode.TooManyRequests); - - await Assert.ThrowsAsync(() => _fhirTransactionExecutor.ExecuteTransactionAsync(new Bundle(), default)); - } - - [Fact] - public async Task GivenRequestFailsWithAnyOtherReason_WhenExecuting_ThenTransactionFailedExceptionShouldBeThrown() - { - var expectedOperationOutcome = new OperationOutcome(); - SetupPostException(HttpStatusCode.NotFound, expectedOperationOutcome); - - TransactionFailedException exception = await Assert.ThrowsAsync( - () => _fhirTransactionExecutor.ExecuteTransactionAsync(new Bundle(), default)); - - Assert.Same(expectedOperationOutcome, exception.OperationOutcome); - } - - [Fact] - public async Task GivenNullResponse_WhenTransactionIsExecuted_ThenInvalidFhirResponseExceptionShouldBeThrown() - { - var bundle = new Bundle(); - - _fhirClient.PostBundleAsync(bundle).Returns(new FhirResponse(new HttpResponseMessage(), null)); - - await Assert.ThrowsAsync(() => _fhirTransactionExecutor.ExecuteTransactionAsync(bundle, default)); - } - - [Fact] - public async Task GivenResponseBundleEntryCountDoesNotMatch_WhenTransactionIsExecuted_ThenInvalidFhirResponseExceptionShouldBeThrown() - { - // Request will have 1 entry but response will have 0. - AddEntryComponentToDefaultRequestBundle(); - - _fhirClient.PostBundleAsync(_defaultRequestBundle).Returns(new FhirResponse(new HttpResponseMessage(HttpStatusCode.OK), new Bundle())); - - await Assert.ThrowsAsync(() => ExecuteTransactionAsync()); - } - - [Fact] - public async Task GivenRequestsIsSuccessful_WhenTransactionIsExecuted_ThenCorrectBundleShouldBeReturned() - { - Bundle expectedBundle = SetupTransaction("200"); - - Bundle actualBundle = await ExecuteTransactionAsync(); - - Assert.Same(expectedBundle, actualBundle); - - // The annotation should be set. - Assert.True(actualBundle.Entry[0].Response.TryGetAnnotation(typeof(HttpStatusCode), out object statusCode)); - - Assert.Equal(HttpStatusCode.OK, (HttpStatusCode)statusCode); - } - - [Fact] - public async Task GivenNullEntryComponent_WhenTransactionIsExecuted_ThenInvalidFhirResponseExceptionShouldBeThrown() - { - var bundle = new Bundle(); - - bundle.Entry.Add(null); - _fhirClient.PostBundleAsync(_defaultRequestBundle).Returns(new FhirResponse(new HttpResponseMessage(HttpStatusCode.OK), bundle)); - - await Assert.ThrowsAsync(() => ExecuteTransactionAsync()); - } - - [Fact] - public async Task GivenNullEntryComponentResponse_WhenTransactionIsExecuted_ThenInvalidFhirResponseExceptionShouldBeThrown() - { - var bundle = new Bundle(); - - bundle.Entry.Add(new Bundle.EntryComponent()); - _fhirClient.PostBundleAsync(_defaultRequestBundle).Returns(new FhirResponse(new HttpResponseMessage(HttpStatusCode.OK), bundle)); - - await Assert.ThrowsAsync(() => ExecuteTransactionAsync()); - } - - [Fact] - public async Task GivenNullEntryComponentResponseStatus_WhenTransactionIsExecuted_ThenInvalidFhirResponseExceptionShouldBeThrown() - { - var bundle = new Bundle(); - - bundle.Entry.Add(new Bundle.EntryComponent() - { - Response = new Bundle.ResponseComponent(), - }); - _fhirClient.PostBundleAsync(_defaultRequestBundle).Returns(new FhirResponse(new HttpResponseMessage(HttpStatusCode.OK), bundle)); - - await Assert.ThrowsAsync(() => ExecuteTransactionAsync()); - } - - [Theory] - [InlineData("")] - [InlineData("10")] - [InlineData("1000")] - [InlineData("10a")] - [InlineData("299")] - public async Task GivenInvalidEntryComponentResponseStatusValue_WhenTransactionIsExecuted_ThenInvalidFhirResponseExceptionShouldBeThrown(string invalidStatus) - { - var bundle = new Bundle(); - - bundle.Entry.Add(new Bundle.EntryComponent() - { - Response = new Bundle.ResponseComponent() - { - Status = invalidStatus, - }, - }); - _fhirClient.PostBundleAsync(_defaultRequestBundle).Returns(new FhirResponse(new HttpResponseMessage(HttpStatusCode.OK), bundle)); - - await Assert.ThrowsAsync(() => ExecuteTransactionAsync()); - } - - private Bundle SetupTransaction(params string[] statusList) - { - for (int i = 0; i < statusList.Length; i++) - { - AddEntryComponentToDefaultRequestBundle(); - } - - Bundle bundle = GenerateBundleWithStatus(statusList); - - _fhirClient.PostBundleAsync(Arg.Any()).Returns(new FhirResponse(new HttpResponseMessage(HttpStatusCode.OK), bundle)); - - return bundle; - } - - private void AddEntryComponentToDefaultRequestBundle() - { - _defaultRequestBundle.Entry.Add(new Bundle.EntryComponent()); - } - - private Task ExecuteTransactionAsync() - => _fhirTransactionExecutor.ExecuteTransactionAsync(_defaultRequestBundle, default); - - private static Bundle GenerateBundleWithStatus(params string[] statusList) - { - var bundle = new Bundle(); - - foreach (string status in statusList) - { - bundle.Entry.Add(new Bundle.EntryComponent() - { - Response = new Bundle.ResponseComponent() - { - Status = status, - }, - }); - } - - return bundle; - } - - private void SetupPostException(HttpStatusCode httpStatusCode, OperationOutcome operationOutcome = null) - { - if (operationOutcome == null) - { - operationOutcome = new OperationOutcome(); - } - - var response = new FhirResponse(new HttpResponseMessage(httpStatusCode), operationOutcome); - _fhirClient.PostBundleAsync(default).ThrowsForAnyArgs(new FhirClientException(response, httpStatusCode)); - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Fhir/ImagingStudyIdentifierUtilityTests.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Fhir/ImagingStudyIdentifierUtilityTests.cs deleted file mode 100644 index 21b3a274a2..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Fhir/ImagingStudyIdentifierUtilityTests.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Microsoft.Health.DicomCast.Core.Features.Fhir; -using Xunit; - -namespace Microsoft.Health.DicomCast.Core.UnitTests.Features.Fhir; - -public class ImagingStudyIdentifierUtilityTests -{ - [Fact] - public void GivenStudyInstanceUid_WhenCreated_ThenCorrectIdentifierShouldBeCreated() - { - var identifier = IdentifierUtility.CreateIdentifier("123"); - - Assert.NotNull(identifier); - Assert.Equal("urn:dicom:uid", identifier.System); - Assert.Equal("urn:oid:123", identifier.Value); - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/ChangeFeedProcessorTests.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/ChangeFeedProcessorTests.cs deleted file mode 100644 index 6b772db2bc..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/ChangeFeedProcessorTests.cs +++ /dev/null @@ -1,507 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Diagnostics; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using FellowOakDicom; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using Microsoft.Extensions.Time.Testing; -using Microsoft.Health.Dicom.Client.Models; -using Microsoft.Health.DicomCast.Core.Configurations; -using Microsoft.Health.DicomCast.Core.Exceptions; -using Microsoft.Health.DicomCast.Core.Features.DicomWeb.Service; -using Microsoft.Health.DicomCast.Core.Features.ExceptionStorage; -using Microsoft.Health.DicomCast.Core.Features.Fhir; -using Microsoft.Health.DicomCast.Core.Features.State; -using Microsoft.Health.DicomCast.Core.Features.Worker; -using Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; -using NSubstitute; -using NSubstitute.ExceptionExtensions; -using Polly.Timeout; -using Xunit; - -namespace Microsoft.Health.DicomCast.Core.UnitTests.Features.Worker; - -public class ChangeFeedProcessorTests -{ - private static readonly CancellationToken DefaultCancellationToken = new CancellationTokenSource().Token; - - private readonly IChangeFeedRetrieveService _changeFeedRetrieveService = Substitute.For(); - private readonly IFhirTransactionPipeline _fhirTransactionPipeline = Substitute.For(); - private readonly ISyncStateService _syncStateService = Substitute.For(); - private readonly IExceptionStore _exceptionStore = Substitute.For(); - private readonly FakeTimeProvider _timeProvider = new FakeTimeProvider(DateTimeOffset.UtcNow); - private readonly IOptions _config = Substitute.For>(); - private readonly DicomCastConfiguration _dicomCastConfiguration = new DicomCastConfiguration(); - private readonly ChangeFeedProcessor _changeFeedProcessor; - - public ChangeFeedProcessorTests() - { - _dicomCastConfiguration.Features.IgnoreJsonParsingErrors = true; - _config.Value.Returns(_dicomCastConfiguration); - - _changeFeedProcessor = new ChangeFeedProcessor( - _changeFeedRetrieveService, - _fhirTransactionPipeline, - _syncStateService, - _exceptionStore, - _timeProvider, - _config, - NullLogger.Instance); - - SetupSyncState(); - } - - [Fact] - public async Task GivenMultipleChangeFeedEntries_WhenProcessed_ThenEachChangeFeedEntryShouldBeProcessed() - { - const long Latest = 3L; - ChangeFeedEntry[] changeFeeds = new[] - { - ChangeFeedGenerator.Generate(1), - ChangeFeedGenerator.Generate(2), - ChangeFeedGenerator.Generate(Latest), - }; - - // Arrange - _changeFeedRetrieveService.RetrieveLatestSequenceAsync(DefaultCancellationToken).Returns(Latest); - - _changeFeedRetrieveService.RetrieveChangeFeedAsync(0, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken).Returns(changeFeeds); - _changeFeedRetrieveService.RetrieveChangeFeedAsync(Latest, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken).Returns(Array.Empty()); - - // Act - await ExecuteProcessAsync(); - - // Assert - await _changeFeedRetrieveService.Received(2).RetrieveLatestSequenceAsync(DefaultCancellationToken); - - await _changeFeedRetrieveService.ReceivedWithAnyArgs(2).RetrieveChangeFeedAsync(default, default, default); - await _changeFeedRetrieveService.Received(1).RetrieveChangeFeedAsync(0, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken); - await _changeFeedRetrieveService.Received(1).RetrieveChangeFeedAsync(Latest, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken); - - await _fhirTransactionPipeline.ReceivedWithAnyArgs(3).ProcessAsync(default, default); - await _fhirTransactionPipeline.Received(1).ProcessAsync(changeFeeds[0], DefaultCancellationToken); - await _fhirTransactionPipeline.Received(1).ProcessAsync(changeFeeds[1], DefaultCancellationToken); - await _fhirTransactionPipeline.Received(1).ProcessAsync(changeFeeds[2], DefaultCancellationToken); - } - - [Fact] - public async Task GivenMalformedChangeFeedEntries_WhenProcessed_BadEntryShouldBeSkipped() - { - const long Latest = 3L; - ChangeFeedEntry[] changeFeeds1 = new[] - { - ChangeFeedGenerator.Generate(1), - }; - ChangeFeedEntry[] changeFeeds2 = new[] - { - ChangeFeedGenerator.Generate(Latest), - }; - - // Arrange - _changeFeedRetrieveService.RetrieveLatestSequenceAsync(DefaultCancellationToken).Returns(Latest); - - // call to retrieve batch has json exception - _changeFeedRetrieveService.RetrieveChangeFeedAsync(0, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken).ThrowsAsync(new JsonException()); - - // get the items individually from the change feed - _changeFeedRetrieveService.RetrieveChangeFeedAsync(0, 1, DefaultCancellationToken).Returns(changeFeeds1); - _changeFeedRetrieveService.RetrieveChangeFeedAsync(1, 1, DefaultCancellationToken).ThrowsAsync(new JsonException()); - _changeFeedRetrieveService.RetrieveChangeFeedAsync(2, 1, DefaultCancellationToken).Returns(changeFeeds2); - _changeFeedRetrieveService.RetrieveChangeFeedAsync(Latest, 1, DefaultCancellationToken).Returns(Array.Empty()); - - _changeFeedRetrieveService.RetrieveChangeFeedAsync(Latest, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken).Returns(Array.Empty()); - - // Act - await ExecuteProcessAsync(); - - // Assert - await _changeFeedRetrieveService.Received(2).RetrieveLatestSequenceAsync(DefaultCancellationToken); - - await _changeFeedRetrieveService.ReceivedWithAnyArgs(6).RetrieveChangeFeedAsync(default, default, default); - await _changeFeedRetrieveService.Received(1).RetrieveChangeFeedAsync(0, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken); - await _changeFeedRetrieveService.Received(1).RetrieveChangeFeedAsync(0, 1, DefaultCancellationToken); - await _changeFeedRetrieveService.Received(1).RetrieveChangeFeedAsync(1, 1, DefaultCancellationToken); - await _changeFeedRetrieveService.Received(1).RetrieveChangeFeedAsync(2, 1, DefaultCancellationToken); - await _changeFeedRetrieveService.Received(1).RetrieveChangeFeedAsync(Latest, 1, DefaultCancellationToken); - await _changeFeedRetrieveService.Received(1).RetrieveChangeFeedAsync(Latest, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken); - - await _fhirTransactionPipeline.ReceivedWithAnyArgs(2).ProcessAsync(default, default); - await _fhirTransactionPipeline.Received(1).ProcessAsync(changeFeeds1[0], DefaultCancellationToken); - await _fhirTransactionPipeline.Received(1).ProcessAsync(changeFeeds2[0], DefaultCancellationToken); - } - - [Fact] - public async Task GivenMalformedChangeFeedEntries_WhenProcessedAndNotIgnoringErrors_ExceptionIsThrown() - { - const long Latest = 3L; - _dicomCastConfiguration.Features.IgnoreJsonParsingErrors = false; - - // Arrange - _changeFeedRetrieveService.RetrieveLatestSequenceAsync(DefaultCancellationToken).Returns(Latest); - - // call to retrieve batch has json exception - _changeFeedRetrieveService.RetrieveChangeFeedAsync(0, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken).ThrowsAsync(new JsonException()); - - // Act - await Assert.ThrowsAsync(() => ExecuteProcessAsync()); - } - - [Fact] - public async Task GivenMultipleChangeFeedEntries_WhenProcessing_ThenItShouldProcessAllPendinChangeFeedEntries() - { - const long Latest = 2L; - ChangeFeedEntry[] changeFeeds1 = new[] - { - ChangeFeedGenerator.Generate(1), - }; - - ChangeFeedEntry[] changeFeeds2 = new[] - { - ChangeFeedGenerator.Generate(Latest), - }; - - // Arrange - _changeFeedRetrieveService.RetrieveLatestSequenceAsync(DefaultCancellationToken).Returns(Latest); - - _changeFeedRetrieveService.RetrieveChangeFeedAsync(0, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken).Returns(changeFeeds1); - _changeFeedRetrieveService.RetrieveChangeFeedAsync(1, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken).Returns(changeFeeds2); - _changeFeedRetrieveService.RetrieveChangeFeedAsync(Latest, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken).Returns(Array.Empty()); - - // Act - await ExecuteProcessAsync(); - - // Assert - await _changeFeedRetrieveService.Received(3).RetrieveLatestSequenceAsync(DefaultCancellationToken); - - await _changeFeedRetrieveService.ReceivedWithAnyArgs(3).RetrieveChangeFeedAsync(default, default, default); - await _changeFeedRetrieveService.Received(1).RetrieveChangeFeedAsync(0, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken); - await _changeFeedRetrieveService.Received(1).RetrieveChangeFeedAsync(1, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken); - await _changeFeedRetrieveService.Received(1).RetrieveChangeFeedAsync(Latest, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken); - - await _fhirTransactionPipeline.ReceivedWithAnyArgs(2).ProcessAsync(default, default); - await _fhirTransactionPipeline.Received(1).ProcessAsync(changeFeeds1[0], DefaultCancellationToken); - await _fhirTransactionPipeline.Received(1).ProcessAsync(changeFeeds2[0], DefaultCancellationToken); - } - - [Fact] - public async Task GivenSkippedSequenceNumbers_WhenProcessing_ThenSkipAheadByLimit() - { - const long PageOneEnd = 5L; - const long Latest = PageOneEnd + ChangeFeedProcessor.DefaultLimit + 1; // Fall onto the next page - ChangeFeedEntry[] changeFeeds1 = new[] - { - ChangeFeedGenerator.Generate(1), - ChangeFeedGenerator.Generate(PageOneEnd), - }; - - ChangeFeedEntry[] changeFeeds3 = new[] - { - ChangeFeedGenerator.Generate(Latest), - }; - - // Arrange - _changeFeedRetrieveService.RetrieveLatestSequenceAsync(DefaultCancellationToken).Returns(PageOneEnd, Latest, Latest, Latest); - - _changeFeedRetrieveService.RetrieveChangeFeedAsync(0, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken).Returns(changeFeeds1); - _changeFeedRetrieveService.RetrieveChangeFeedAsync(PageOneEnd, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken).Returns(Array.Empty()); - _changeFeedRetrieveService.RetrieveChangeFeedAsync(PageOneEnd + ChangeFeedProcessor.DefaultLimit, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken).Returns(changeFeeds3); - _changeFeedRetrieveService.RetrieveChangeFeedAsync(Latest, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken).Returns(Array.Empty()); - - // Act - await ExecuteProcessAsync(); - - // Assert - await _changeFeedRetrieveService.Received(4).RetrieveLatestSequenceAsync(DefaultCancellationToken); - - await _changeFeedRetrieveService.ReceivedWithAnyArgs(4).RetrieveChangeFeedAsync(default, default, default); - await _changeFeedRetrieveService.RetrieveChangeFeedAsync(0, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken); - await _changeFeedRetrieveService.RetrieveChangeFeedAsync(PageOneEnd, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken); - await _changeFeedRetrieveService.RetrieveChangeFeedAsync(PageOneEnd + ChangeFeedProcessor.DefaultLimit, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken); - await _changeFeedRetrieveService.RetrieveChangeFeedAsync(Latest, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken); - - await _fhirTransactionPipeline.ReceivedWithAnyArgs(3).ProcessAsync(default, default); - await _fhirTransactionPipeline.Received(1).ProcessAsync(changeFeeds1[0], DefaultCancellationToken); - await _fhirTransactionPipeline.Received(1).ProcessAsync(changeFeeds1[1], DefaultCancellationToken); - await _fhirTransactionPipeline.Received(1).ProcessAsync(changeFeeds3[0], DefaultCancellationToken); - } - - [Fact] - public async Task WhenThrowUnhandledError_ErrorThrown() - { - ChangeFeedEntry[] changeFeeds1 = new[] - { - ChangeFeedGenerator.Generate(1), - }; - - // Arrange - _changeFeedRetrieveService.RetrieveChangeFeedAsync(0, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken).Returns(changeFeeds1); - _fhirTransactionPipeline.When(pipeline => pipeline.ProcessAsync(Arg.Any(), Arg.Any())).Do(pipeline => { throw new Exception(); }); - - // Act - await Assert.ThrowsAsync(() => ExecuteProcessAsync()); - - // Assert - await _changeFeedRetrieveService.Received(1).RetrieveLatestSequenceAsync(DefaultCancellationToken); - await _changeFeedRetrieveService.Received(1).RetrieveChangeFeedAsync(0, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken); - await _fhirTransactionPipeline.Received(1).ProcessAsync(changeFeeds1[0], DefaultCancellationToken); - } - - [Fact] - public async Task WhenThrowTimeoutRejectedException_ExceptionNotThrown() - { - ChangeFeedEntry[] changeFeeds1 = new[] - { - ChangeFeedGenerator.Generate(1), - }; - - // Arrange - _changeFeedRetrieveService.RetrieveLatestSequenceAsync(DefaultCancellationToken).Returns(1L); - - _changeFeedRetrieveService.RetrieveChangeFeedAsync(0, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken).Returns(changeFeeds1); - _changeFeedRetrieveService.RetrieveChangeFeedAsync(1, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken).Returns(Array.Empty()); - - _fhirTransactionPipeline.When(pipeline => pipeline.ProcessAsync(Arg.Any(), Arg.Any())).Do(pipeline => { throw new TimeoutRejectedException(); }); - - // Act - await ExecuteProcessAsync(); - - // Assert - await _changeFeedRetrieveService.Received(2).RetrieveLatestSequenceAsync(DefaultCancellationToken); - - await _changeFeedRetrieveService.ReceivedWithAnyArgs(2).RetrieveChangeFeedAsync(default, default, default); - await _changeFeedRetrieveService.Received(1).RetrieveChangeFeedAsync(0, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken); - await _changeFeedRetrieveService.Received(1).RetrieveChangeFeedAsync(1, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken); - - await _fhirTransactionPipeline.Received(1).ProcessAsync(changeFeeds1[0], DefaultCancellationToken); - } - - [Theory] - [InlineData(nameof(DicomTagException))] - [InlineData(nameof(MissingRequiredDicomTagException))] - public async Task WhenThrowDicomTagException_ExceptionNotThrown(string exception) - { - ChangeFeedEntry[] changeFeeds1 = new[] - { - ChangeFeedGenerator.Generate(1), - }; - - // Arrange - _changeFeedRetrieveService.RetrieveLatestSequenceAsync(DefaultCancellationToken).Returns(1L); - - _changeFeedRetrieveService.RetrieveChangeFeedAsync(0, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken).Returns(changeFeeds1); - _changeFeedRetrieveService.RetrieveChangeFeedAsync(1, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken).Returns(Array.Empty()); - - _fhirTransactionPipeline.When(pipeline => pipeline.ProcessAsync(Arg.Any(), Arg.Any())).Do(pipeline => { ThrowDicomTagException(exception); }); - - // Act - await ExecuteProcessAsync(); - - // Assert - await _changeFeedRetrieveService.Received(2).RetrieveLatestSequenceAsync(DefaultCancellationToken); - - await _changeFeedRetrieveService.ReceivedWithAnyArgs(2).RetrieveChangeFeedAsync(default, default, default); - await _changeFeedRetrieveService.Received(1).RetrieveChangeFeedAsync(0, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken); - await _changeFeedRetrieveService.Received(1).RetrieveChangeFeedAsync(1, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken); - - await _fhirTransactionPipeline.Received(1).ProcessAsync(changeFeeds1[0], DefaultCancellationToken); - } - - [Fact] - public async Task WhenMissingRequiredDicomTagException_ExceptionNotThrown() - { - ChangeFeedEntry[] changeFeeds1 = new[] - { - ChangeFeedGenerator.Generate(1), - }; - - // Arrange - _changeFeedRetrieveService.RetrieveLatestSequenceAsync(DefaultCancellationToken).Returns(1L); - - _changeFeedRetrieveService.RetrieveChangeFeedAsync(0, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken).Returns(changeFeeds1); - _changeFeedRetrieveService.RetrieveChangeFeedAsync(1, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken).Returns(Array.Empty()); - - _fhirTransactionPipeline.When(pipeline => pipeline.ProcessAsync(Arg.Any(), Arg.Any())).Do(pipeline => { throw new MissingRequiredDicomTagException(nameof(DicomTag.PatientID)); }); - - // Act - await ExecuteProcessAsync(); - - // Assert - await _changeFeedRetrieveService.Received(2).RetrieveLatestSequenceAsync(DefaultCancellationToken); - - await _changeFeedRetrieveService.ReceivedWithAnyArgs(2).RetrieveChangeFeedAsync(default, default, default); - await _changeFeedRetrieveService.Received(1).RetrieveChangeFeedAsync(0, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken); - await _changeFeedRetrieveService.Received(1).RetrieveChangeFeedAsync(1, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken); - - await _fhirTransactionPipeline.Received(1).ProcessAsync(changeFeeds1[0], DefaultCancellationToken); - } - - [Fact] - public async Task WhenThrowFhirNonRetryableException_ExceptionNotThrown() - { - ChangeFeedEntry[] changeFeeds1 = new[] - { - ChangeFeedGenerator.Generate(1), - }; - - // Arrange - _changeFeedRetrieveService.RetrieveLatestSequenceAsync(DefaultCancellationToken).Returns(1L); - _changeFeedRetrieveService.RetrieveChangeFeedAsync(0, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken).Returns(changeFeeds1); - _changeFeedRetrieveService.RetrieveChangeFeedAsync(1, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken).Returns(Array.Empty()); - - _fhirTransactionPipeline.When(pipeline => pipeline.ProcessAsync(Arg.Any(), Arg.Any())).Do(pipeline => { throw new FhirNonRetryableException("exception"); }); - - // Act - await ExecuteProcessAsync(); - - // Assert - await _changeFeedRetrieveService.Received(2).RetrieveLatestSequenceAsync(DefaultCancellationToken); - - await _changeFeedRetrieveService.ReceivedWithAnyArgs(2).RetrieveChangeFeedAsync(default, default, default); - await _changeFeedRetrieveService.Received(1).RetrieveChangeFeedAsync(0, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken); - await _changeFeedRetrieveService.Received(1).RetrieveChangeFeedAsync(1, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken); - - await _fhirTransactionPipeline.Received(1).ProcessAsync(changeFeeds1[0], DefaultCancellationToken); - } - - [Fact] - public async Task GivenMultipleChangeFeedEntries_WhenProcessing_ThenPollIntervalShouldBeHonored() - { - var pollIntervalDuringCatchup = TimeSpan.FromMilliseconds(50); - - ChangeFeedEntry[] changeFeeds1 = new[] - { - ChangeFeedGenerator.Generate(1), - }; - - ChangeFeedEntry[] changeFeeds2 = new[] - { - ChangeFeedGenerator.Generate(2), - }; - - // Arrange - _changeFeedRetrieveService.RetrieveLatestSequenceAsync(DefaultCancellationToken).Returns(2L); - _changeFeedRetrieveService.RetrieveChangeFeedAsync(0, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken).Returns(changeFeeds1); - _changeFeedRetrieveService.RetrieveChangeFeedAsync(1, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken).Returns(changeFeeds2); - _changeFeedRetrieveService.RetrieveChangeFeedAsync(2, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken).Returns(Array.Empty()); - - var stopwatch = new Stopwatch(); - - _fhirTransactionPipeline.When(processor => processor.ProcessAsync(changeFeeds1[0], DefaultCancellationToken)).Do(_ => stopwatch.Start()); - _fhirTransactionPipeline.When(processor => processor.ProcessAsync(changeFeeds2[0], DefaultCancellationToken)).Do(_ => stopwatch.Stop()); - - // Execute Process when no poll interval is defined. - await ExecuteProcessAsync(); - - // Using stopwatch.Elapsed to get total time elapsed when no poll interval is defined. - TimeSpan totalTimeTakenWithNoPollInterval = stopwatch.Elapsed; - - stopwatch.Reset(); - - // Execute process when poll interval is defined. - await ExecuteProcessAsync(pollIntervalDuringCatchup); - - // Using stopwatch.Elapsed to get total time elapsed when poll interval is defined. - TimeSpan totalTimeTakenWithPollInterval = stopwatch.Elapsed; - - Assert.True(totalTimeTakenWithPollInterval >= totalTimeTakenWithNoPollInterval); - } - - [Fact] - public async Task GivenDefaultState_WhenProcessed_ThenSyncStateShouldNotBeUpdated() - { - // Arrange - _syncStateService.GetSyncStateAsync(DefaultCancellationToken).Returns(SyncState.CreateInitialSyncState()); - _changeFeedRetrieveService.RetrieveLatestSequenceAsync(DefaultCancellationToken).Returns(0L); - _changeFeedRetrieveService.RetrieveChangeFeedAsync(0, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken).Returns(Array.Empty()); - - // Act - await ExecuteProcessAsync(); - - // Assert - await _syncStateService.Received(1).GetSyncStateAsync(DefaultCancellationToken); - await _changeFeedRetrieveService.Received(1).RetrieveLatestSequenceAsync(DefaultCancellationToken); - await _changeFeedRetrieveService.Received(1).RetrieveChangeFeedAsync(0, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken); - await _fhirTransactionPipeline.DidNotReceiveWithAnyArgs().ProcessAsync(default, default); - await _syncStateService.DidNotReceiveWithAnyArgs().UpdateSyncStateAsync(default, default); - } - - [Fact] - public async Task GivenNoChangeFeed_WhenProcessed_ThenSyncStateShouldNotBeUpdated() - { - const long Sequence = 27L; - - // Arrange - _syncStateService.GetSyncStateAsync(DefaultCancellationToken).Returns(new SyncState(Sequence, DateTimeOffset.UtcNow)); - _changeFeedRetrieveService.RetrieveLatestSequenceAsync(DefaultCancellationToken).Returns(Sequence); - _changeFeedRetrieveService.RetrieveChangeFeedAsync(Sequence, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken).Returns(Array.Empty()); - - // Act - await ExecuteProcessAsync(); - - // Assert - await _syncStateService.Received(1).GetSyncStateAsync(DefaultCancellationToken); - await _changeFeedRetrieveService.Received(1).RetrieveLatestSequenceAsync(DefaultCancellationToken); - await _changeFeedRetrieveService.Received(1).RetrieveChangeFeedAsync(Sequence, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken); - await _fhirTransactionPipeline.DidNotReceiveWithAnyArgs().ProcessAsync(default, default); - await _syncStateService.DidNotReceiveWithAnyArgs().UpdateSyncStateAsync(default, default); - } - - [Fact] - public async Task GivenAllChangeFeedEntriesAreSuccess_WhenProcessed_ThenSyncStateShouldBeUpdated() - { - const long expectedSequence = 10; - - ChangeFeedEntry[] changeFeeds = [ChangeFeedGenerator.Generate(expectedSequence)]; - - // Arrange - _changeFeedRetrieveService.RetrieveLatestSequenceAsync(DefaultCancellationToken).Returns(expectedSequence); - _changeFeedRetrieveService.RetrieveChangeFeedAsync(0, ChangeFeedProcessor.DefaultLimit, DefaultCancellationToken).Returns(changeFeeds); - - var instant = DateTimeOffset.UtcNow.AddHours(1); - _timeProvider.SetUtcNow(instant); - - // Act - await ExecuteProcessAsync(); - - // Assert - await _syncStateService - .Received(1) - .UpdateSyncStateAsync(Arg.Is(syncState => syncState != null && syncState.SyncedSequence == expectedSequence && syncState.SyncedDate == instant), DefaultCancellationToken); - } - - private void SetupSyncState(long syncedSequence = 0, DateTimeOffset? syncedDate = null) - { - var syncState = new SyncState(syncedSequence, syncedDate == null ? DateTimeOffset.MinValue : syncedDate.Value); - - _syncStateService.GetSyncStateAsync(DefaultCancellationToken).Returns(syncState); - } - - private async Task ExecuteProcessAsync(TimeSpan? pollIntervalDuringCatchup = null) - { - if (pollIntervalDuringCatchup == null) - { - pollIntervalDuringCatchup = TimeSpan.Zero; - } - - await _changeFeedProcessor.ProcessAsync(pollIntervalDuringCatchup.Value, DefaultCancellationToken); - } - - private static void ThrowDicomTagException(string exception) - { - if (exception.Equals(nameof(DicomTagException))) - { - throw new DicomTagException("exception"); - } - else if (exception.Equals(nameof(MissingRequiredDicomTagException))) - { - throw new MissingRequiredDicomTagException(nameof(DicomTag.PatientID)); - } - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/DicomCastWorkerTests.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/DicomCastWorkerTests.cs deleted file mode 100644 index 5963ac89f6..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/DicomCastWorkerTests.cs +++ /dev/null @@ -1,143 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using Microsoft.Health.DicomCast.Core.Configurations; -using Microsoft.Health.DicomCast.Core.Features.Fhir; -using Microsoft.Health.DicomCast.Core.Features.Worker; -using NSubstitute; -using OpenTelemetry; -using OpenTelemetry.Metrics; -using Xunit; - -namespace Microsoft.Health.DicomCast.Core.UnitTests.Features.Worker; - -public class DicomCastWorkerTests -{ - private const int DefaultNumberOfInvocations = 2; - - private readonly DicomCastWorkerConfiguration _dicomCastWorkerConfiguration = new DicomCastWorkerConfiguration(); - private readonly IChangeFeedProcessor _changeFeedProcessor; - private readonly IHostApplicationLifetime _hostApplication; - private readonly DicomCastWorker _dicomCastWorker; - private readonly IFhirService _fhirService; - private readonly DicomCastMeter _dicomCastMeter; - - private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); - private readonly CancellationToken _cancellationToken; - - private MeterProvider _meterProvider; - private List _exportedItems; - - public DicomCastWorkerTests() - { - _cancellationToken = _cancellationTokenSource.Token; - - _dicomCastWorkerConfiguration.PollInterval = TimeSpan.Zero; - - _changeFeedProcessor = Substitute.For(); - - _hostApplication = Substitute.For(); - - _fhirService = Substitute.For(); - - _dicomCastMeter = new DicomCastMeter(); - - _dicomCastWorker = new DicomCastWorker( - Options.Create(_dicomCastWorkerConfiguration), - _changeFeedProcessor, - NullLogger.Instance, - _hostApplication, - _fhirService, - _dicomCastMeter); - - InitializeMetricExporter(); - } - - [Fact] - public async Task GivenWorkerIsBeingCanceled_WhenExecuting_ThenWorkerShouldBeCancelled() - { - int invocationCount = 0; - - _changeFeedProcessor.When(processor => processor.ProcessAsync(_dicomCastWorkerConfiguration.PollIntervalDuringCatchup, _cancellationToken)) - .Do(_ => - { - if (invocationCount++ == DefaultNumberOfInvocations) - { - _cancellationTokenSource.Cancel(); - - throw new TaskCanceledException(); - } - }); - - await _dicomCastWorker.ExecuteAsync(_cancellationToken); - - await _changeFeedProcessor.Received(invocationCount).ProcessAsync(_dicomCastWorkerConfiguration.PollIntervalDuringCatchup, _cancellationToken); - } - - [Fact] - public async Task GivenWorkerIsBeingCanceled_WhenExecutingAndFailed_ThenProperMetricsisLogged() - { - _changeFeedProcessor.When(processor => processor.ProcessAsync(_dicomCastWorkerConfiguration.PollIntervalDuringCatchup, _cancellationToken)) - .Do(_ => - { - throw new TaskCanceledException(); - }); - - await _dicomCastWorker.ExecuteAsync(_cancellationToken); - - _meterProvider.ForceFlush(); - - Assert.NotEmpty(_exportedItems.Where(item => item.Name.Equals("CastingFailedForOtherReasons"))); - } - - [Fact(Skip = "Flaky test, bug: https://microsofthealth.visualstudio.com/Health/_boards/board/t/Medical%20Imaging/Stories/?workitem=78349")] - public async Task GivenWorker_WhenExecuting_ThenPollIntervalShouldBeHonored() - { - var pollInterval = TimeSpan.FromMilliseconds(50); - - _dicomCastWorkerConfiguration.PollInterval = pollInterval; - - int invocationCount = 0; - - var stopwatch = new Stopwatch(); - - _changeFeedProcessor.When(processor => processor.ProcessAsync(_dicomCastWorkerConfiguration.PollIntervalDuringCatchup, _cancellationToken)) - .Do(_ => - { - if (invocationCount++ == 0) - { - stopwatch.Start(); - } - else - { - stopwatch.Stop(); - - _cancellationTokenSource.Cancel(); - } - }); - - await _dicomCastWorker.ExecuteAsync(_cancellationToken); - - Assert.True(stopwatch.ElapsedMilliseconds >= pollInterval.TotalMilliseconds); - } - - private void InitializeMetricExporter() - { - _exportedItems = new List(); - _meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter("Microsoft.Health.DicomCast", "1.0") - .AddInMemoryExporter(_exportedItems) - .Build(); - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/EndpointPipelineStepTests.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/EndpointPipelineStepTests.cs deleted file mode 100644 index f531837f70..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/EndpointPipelineStepTests.cs +++ /dev/null @@ -1,100 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Linq; -using System.Threading; -using Hl7.Fhir.Model; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using Microsoft.Health.DicomCast.Core.Configurations; -using Microsoft.Health.DicomCast.Core.Extensions; -using Microsoft.Health.DicomCast.Core.Features.Fhir; -using Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; -using NSubstitute; -using Xunit; -using Task = System.Threading.Tasks.Task; - -namespace Microsoft.Health.DicomCast.Core.UnitTests.Features.Worker.FhirTransaction; - -public class EndpointPipelineStepTests -{ - private const string EndpointConnectionTypeSystem = "http://terminology.hl7.org/CodeSystem/endpoint-connection-type"; - private const string EndpointConnectionTypeCode = "dicom-wado-rs"; - private const string EndpointName = "DICOM WADO-RS endpoint https://dicom/"; - private const string EndpointPayloadTypeText = "DICOM WADO-RS"; - private const string DicomMimeType = "application/dicom"; - - private const string DefaultDicomWebEndpoint = "https://dicom/"; - - private static readonly CancellationToken DefaultCancellationToken = new CancellationTokenSource().Token; - - private readonly DicomWebConfiguration _configuration; - private readonly EndpointPipelineStep _endpointPipeline; - private readonly IFhirService _fhirService; - - public EndpointPipelineStepTests() - { - _configuration = new DicomWebConfiguration() { Endpoint = new System.Uri(DefaultDicomWebEndpoint), }; - - IOptions optionsConfiguration = Options.Create(_configuration); - - _fhirService = Substitute.For(); - - _endpointPipeline = new EndpointPipelineStep(optionsConfiguration, _fhirService, NullLogger.Instance); - } - - [Fact] - public async Task GivenEndpointDoesNotAlreadyExist_WhenRequestIsPrepared_ThenCorrentRequestEntryShouldBeCreated() - { - var context = new FhirTransactionContext(ChangeFeedGenerator.Generate()); - - await _endpointPipeline.PrepareRequestAsync(context, DefaultCancellationToken); - - FhirTransactionRequestEntry actualEndpointEntry = context.Request.Endpoint; - - ValidationUtility.ValidateRequestEntryMinimumRequirementForWithChange(FhirTransactionRequestMode.Create, "Endpoint", Bundle.HTTPVerb.POST, actualEndpointEntry); - - Assert.Equal($"name={EndpointName}&connection-type={EndpointConnectionTypeSystem}|{EndpointConnectionTypeCode}", actualEndpointEntry.Request.IfNoneExist); - - Endpoint endpoint = Assert.IsType(actualEndpointEntry.Resource); - - Assert.Equal(EndpointName, endpoint.Name); - Assert.Equal(Endpoint.EndpointStatus.Active, endpoint.Status); - Assert.NotNull(endpoint.ConnectionType); - Assert.Equal(EndpointConnectionTypeSystem, endpoint.ConnectionType.System); - Assert.Equal(EndpointConnectionTypeCode, endpoint.ConnectionType.Code); - Assert.Equal(_configuration.Endpoint.ToString(), endpoint.Address); - Assert.Equal(EndpointPayloadTypeText, endpoint.PayloadType.First().Text); - Assert.Equal(new[] { DicomMimeType }, endpoint.PayloadMimeType); - } - - [Fact] - public async Task GivenAnExistingEndpointWithMatchingAddress_WhenRequestIsPrepared_ThenCorrectRequestEntryShouldBeCreated() - { - var context = new FhirTransactionContext(ChangeFeedGenerator.Generate()); - - Endpoint endpoint = FhirResourceBuilder.CreateEndpointResource(address: DefaultDicomWebEndpoint); - - _fhirService.RetrieveEndpointAsync(Arg.Any(), DefaultCancellationToken).Returns(endpoint); - - await _endpointPipeline.PrepareRequestAsync(context, DefaultCancellationToken); - - FhirTransactionRequestEntry actualEndPointEntry = context.Request.Endpoint; - - ValidationUtility.ValidateRequestEntryMinimumRequirementForNoChange(endpoint.ToServerResourceId(), actualEndPointEntry); - } - - [Fact] - public async Task GivenAnExistingEndpointWithDifferentAddress_WhenRequestIsPrepared_ThenFhirResourceValidationExceptionShouldBeThrown() - { - var context = new FhirTransactionContext(ChangeFeedGenerator.Generate()); - - Endpoint endpoint = FhirResourceBuilder.CreateEndpointResource(address: "https://dicom2"); - - _fhirService.RetrieveEndpointAsync(Arg.Any(), DefaultCancellationToken).Returns(endpoint); - - await Assert.ThrowsAsync(() => _endpointPipeline.PrepareRequestAsync(context, DefaultCancellationToken)); - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/FhirResourceBuilder.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/FhirResourceBuilder.cs deleted file mode 100644 index 72f38e3174..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/FhirResourceBuilder.cs +++ /dev/null @@ -1,70 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using Hl7.Fhir.Model; -using Microsoft.Health.DicomCast.Core.Features.Fhir; -using Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -namespace Microsoft.Health.DicomCast.Core.UnitTests.Features.Worker.FhirTransaction; - -public class FhirResourceBuilder -{ - public static ImagingStudy CreateNewImagingStudy(string studyInstanceUid, List seriesInstanceUidList, List sopInstanceUidList, string patientResourceId, string source = "defaultSouce") - { - // Create a new ImagingStudy - ImagingStudy study = new ImagingStudy - { - Id = "123", - Status = ImagingStudy.ImagingStudyStatus.Available, - Subject = new ResourceReference(patientResourceId), - Meta = new Meta() - { - VersionId = "1", - Source = source, - }, - }; - - foreach (string seriesInstanceUid in seriesInstanceUidList) - { - ImagingStudy.SeriesComponent series = new ImagingStudy.SeriesComponent() - { - Uid = seriesInstanceUid, - }; - - foreach (string sopInstanceUid in sopInstanceUidList) - { - ImagingStudy.InstanceComponent instance = new ImagingStudy.InstanceComponent() - { - Uid = sopInstanceUid, - }; - - series.Instance.Add(instance); - } - - study.Series.Add(series); - } - - study.Identifier.Add(IdentifierUtility.CreateIdentifier(studyInstanceUid)); - - return study; - } - - public static Endpoint CreateEndpointResource(string id = null, string name = null, string connectionSystem = null, string connectionCode = null, string address = null) - { - return new Endpoint() - { - Id = id ?? "1234", - Name = name ?? FhirTransactionConstants.EndpointName, - Status = Endpoint.EndpointStatus.Active, - ConnectionType = new Coding() - { - System = connectionSystem ?? FhirTransactionConstants.EndpointConnectionTypeSystem, - Code = connectionCode ?? FhirTransactionConstants.EndpointConnectionTypeCode, - }, - Address = address ?? "https://dicom/", - }; - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/FhirTransactionContextBuilder.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/FhirTransactionContextBuilder.cs deleted file mode 100644 index 13dfbda34a..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/FhirTransactionContextBuilder.cs +++ /dev/null @@ -1,63 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using FellowOakDicom; -using Hl7.Fhir.Model; -using Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -namespace Microsoft.Health.DicomCast.Core.UnitTests.Features.Worker.FhirTransaction; - -public class FhirTransactionContextBuilder -{ - public static readonly DateTime DefaultStudyDateTime = new DateTime(1974, 7, 10, 7, 10, 24); - public static readonly DateTime DefaultSeriesDateTime = new DateTime(1974, 8, 10, 8, 10, 24); - public const string DefaultSOPClassUID = "4444"; - public const string DefaultStudyDescription = "Study Description"; - public const string DefaultSeriesDescription = "Series Description"; - public const string DefaultModalitiesInStudy = "MODALITY"; - public const string DefaultModality = "MODALITY"; - public const string DefaultSeriesNumber = "1"; - public const string DefaultInstanceNumber = "1"; - public const string DefaultAccessionNumber = "1"; - - public static DicomDataset CreateDicomDataset(string sopClassUid = null, string studyDescription = null, string seriesDescrition = null, string modalityInStudy = null, string modalityInSeries = null, string seriesNumber = null, string instanceNumber = null, string accessionNumber = null) - { - var ds = new DicomDataset(DicomTransferSyntax.ExplicitVRLittleEndian) - { - { DicomTag.SOPClassUID, sopClassUid ?? DefaultSOPClassUID }, - { DicomTag.StudyDate, DefaultStudyDateTime }, - { DicomTag.StudyTime, DefaultStudyDateTime }, - { DicomTag.SeriesDate, DefaultSeriesDateTime }, - { DicomTag.SeriesTime, DefaultSeriesDateTime }, - { DicomTag.StudyDescription, studyDescription ?? DefaultStudyDescription }, - { DicomTag.SeriesDescription, seriesDescrition ?? DefaultSeriesDescription }, - { DicomTag.ModalitiesInStudy, modalityInStudy ?? DefaultModalitiesInStudy }, - { DicomTag.Modality, modalityInSeries ?? DefaultModality }, - { DicomTag.SeriesNumber, seriesNumber ?? DefaultSeriesNumber }, - { DicomTag.InstanceNumber, instanceNumber ?? DefaultInstanceNumber }, - { DicomTag.AccessionNumber, accessionNumber ?? DefaultAccessionNumber }, - { DicomTag.StudyInstanceUID, DicomUID.Generate().UID }, - { DicomTag.SeriesInstanceUID, DicomUID.Generate().UID }, - { DicomTag.SOPInstanceUID, DicomUID.Generate().UID }, - }; - - return ds; - } - - public static FhirTransactionContext DefaultFhirTransactionContext(DicomDataset metadata = null) - { - var context = new FhirTransactionContext(ChangeFeedGenerator.Generate(metadata: metadata ?? CreateDicomDataset())) - { - UtcDateTimeOffset = TimeSpan.Zero, - }; - - context.Request.Patient = FhirTransactionRequestEntryGenerator.GenerateDefaultCreateRequestEntry(); - context.Request.ImagingStudy = FhirTransactionRequestEntryGenerator.GenerateDefaultCreateRequestEntry(); - context.Request.Endpoint = FhirTransactionRequestEntryGenerator.GenerateDefaultCreateRequestEntry(); - - return context; - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/FhirTransactionPipelineTests.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/FhirTransactionPipelineTests.cs deleted file mode 100644 index f7758a29be..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/FhirTransactionPipelineTests.cs +++ /dev/null @@ -1,326 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Hl7.Fhir.Model; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Client.Models; -using Microsoft.Health.DicomCast.Core.Configurations; -using Microsoft.Health.DicomCast.Core.Exceptions; -using Microsoft.Health.DicomCast.Core.Features.ExceptionStorage; -using Microsoft.Health.DicomCast.Core.Features.Fhir; -using Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; -using NSubstitute; -using NSubstitute.ExceptionExtensions; -using Polly.Timeout; -using Xunit; -using Task = System.Threading.Tasks.Task; - -namespace Microsoft.Health.DicomCast.Core.UnitTests.Features.Worker.FhirTransaction; - -public class FhirTransactionPipelineTests -{ - private readonly IList _fhirTransactionPipelineSteps = new List(); - private readonly FhirTransactionRequestResponsePropertyAccessors _fhirTransactionRequestResponsePropertyAccessors = new FhirTransactionRequestResponsePropertyAccessors(); - private readonly IFhirTransactionExecutor _fhirTransactionExecutor = Substitute.For(); - private readonly IExceptionStore _exceptionStore = Substitute.For(); - - private readonly FhirTransactionPipeline _fhirTransactionPipeline; - - private readonly IFhirTransactionPipelineStep _captureFhirTransactionContextStep = Substitute.For(); - - private FhirTransactionContext _capturedFhirTransactionContext; - - public FhirTransactionPipelineTests() - { - // Use this step to capture the context. The same context will be used across all steps. - _captureFhirTransactionContextStep.When(pipeline => pipeline.PrepareRequestAsync(Arg.Any(), Arg.Any())) - .Do(callback => - { - FhirTransactionContext context = callback.ArgAt(0); - - _capturedFhirTransactionContext = context; - }); - - _fhirTransactionPipelineSteps.Add(_captureFhirTransactionContextStep); - - RetryConfiguration retryConfiguration = new RetryConfiguration(); - retryConfiguration.TotalRetryDuration = new TimeSpan(0, 0, 15); - - _fhirTransactionPipeline = new FhirTransactionPipeline( - _fhirTransactionPipelineSteps, - _fhirTransactionRequestResponsePropertyAccessors, - _fhirTransactionExecutor, - _exceptionStore, - Options.Create(retryConfiguration), - NullLogger.Instance); - } - - [Theory] - [InlineData(FhirTransactionRequestMode.Create)] - [InlineData(FhirTransactionRequestMode.Update)] - public async Task GivenAResourceToProcess_WhenProcessed_ThenTransactionShouldBeExecuted(FhirTransactionRequestMode requestMode) - { - // Setup the pipeline step to simulate creating/updating patient. - var patientRequest = new FhirTransactionRequestEntry( - requestMode, - new Bundle.RequestComponent(), - new ClientResourceId(), - new Patient()); - - var pipelineStep = new MockFhirTransactionPipelineStep() - { - OnPrepareRequestAsyncCalled = (context, cancellationToken) => - { - context.Request.Patient = patientRequest; - }, - }; - - _fhirTransactionPipelineSteps.Add(pipelineStep); - - // Setup the transaction executor to return response. - var responseBundle = new Bundle(); - - var responseEntry = new Bundle.EntryComponent() - { - Response = new Bundle.ResponseComponent(), - Resource = new Patient(), - }; - - responseBundle.Entry.Add(responseEntry); - - _fhirTransactionExecutor.ExecuteTransactionAsync( - Arg.Any(), - Arg.Any()) - .Returns(call => - { - // Make sure the request bundle is correct. - Bundle requestBundle = call.ArgAt(0); - - Assert.NotNull(requestBundle); - Assert.Equal(Bundle.BundleType.Transaction, requestBundle.Type); - - Assert.Collection( - requestBundle.Entry, - entry => - { - Assert.Equal(patientRequest.ResourceId.ToString(), entry.FullUrl); - Assert.Equal(patientRequest.Request, entry.Request); - Assert.Equal(patientRequest.Resource, entry.Resource); - }); - - return responseBundle; - }); - - // Process - await _fhirTransactionPipeline.ProcessAsync(ChangeFeedGenerator.Generate(), CancellationToken.None); - - // The response should have been processed. - Assert.NotNull(_capturedFhirTransactionContext); - - FhirTransactionResponseEntry patientResponse = _capturedFhirTransactionContext.Response.Patient; - - Assert.NotNull(patientResponse); - Assert.Equal(responseEntry.Response, patientResponse.Response); - Assert.Equal(responseEntry.Resource, patientResponse.Resource); - } - - [Fact] - public async Task WhenThrowAnExceptionInProcess_ThrowTheSameException() - { - var pipelineStep = new MockFhirTransactionPipelineStep() - { - OnPrepareRequestAsyncCalled = (context, cancellationToken) => - { - throw new Exception(); - }, - }; - - _fhirTransactionPipelineSteps.Add(pipelineStep); - - // Process - await Assert.ThrowsAsync(() => _fhirTransactionPipeline.ProcessAsync(ChangeFeedGenerator.Generate(), CancellationToken.None)); - } - - [Fact] - public async Task GivenNoResourceToProcess_WhenProcessed_ThenTransactionShouldBeExecuted() - { - // Setup the pipeline step to simulate no requests. - IFhirTransactionPipelineStep pipelineStep = Substitute.For(); - - _fhirTransactionPipelineSteps.Add(pipelineStep); - - // Process - await _fhirTransactionPipeline.ProcessAsync(ChangeFeedGenerator.Generate(), CancellationToken.None); - - // There should not be any response. - pipelineStep.DidNotReceiveWithAnyArgs().ProcessResponse(default); - } - - [Fact] - public async Task GivenResourcesInMixedState_WhenProcessed_ThenOnlyResourceWithChangesShouldBeProcessed() - { - // Setup the pipeline step to simulate updating an existing patient. - FhirTransactionRequestEntry patientRequest = FhirTransactionRequestEntryGenerator.GenerateDefaultUpdateRequestEntry( - new ServerResourceId(ResourceType.Patient, "p1")); - - var patientStep = new MockFhirTransactionPipelineStep() - { - OnPrepareRequestAsyncCalled = (context, cancellationToken) => - { - context.Request.Patient = patientRequest; - }, - }; - - // Setup the pipeline step to simulate no update to endpoint. - FhirTransactionRequestEntry endpointRequest = FhirTransactionRequestEntryGenerator.GenerateDefaultNoChangeRequestEntry( - new ServerResourceId(ResourceType.Endpoint, "123")); - - var endpointStep = new MockFhirTransactionPipelineStep() - { - OnPrepareRequestAsyncCalled = (context, cancellationToken) => - { - context.Request.Endpoint = endpointRequest; - }, - }; - - // Setup the pipeline step to simulate creating a new imaging study. - FhirTransactionRequestEntry imagingStudyRequest = FhirTransactionRequestEntryGenerator.GenerateDefaultCreateRequestEntry(); - - var imagingStudyStep = new MockFhirTransactionPipelineStep() - { - OnPrepareRequestAsyncCalled = (context, cancellationToken) => - { - context.Request.ImagingStudy = imagingStudyRequest; - }, - }; - - _fhirTransactionPipelineSteps.Add(patientStep); - _fhirTransactionPipelineSteps.Add(endpointStep); - _fhirTransactionPipelineSteps.Add(imagingStudyStep); - - // Setup the transaction executor to return response. - // The properties will be processed in alphabetical order. - var responseBundle = new Bundle(); - - var imagingStudyResponseEntry = new Bundle.EntryComponent() - { - Response = new Bundle.ResponseComponent(), - Resource = new ImagingStudy(), - }; - - var patientResponseEntry = new Bundle.EntryComponent() - { - Response = new Bundle.ResponseComponent(), - Resource = new Patient(), - }; - - responseBundle.Entry.Add(imagingStudyResponseEntry); - responseBundle.Entry.Add(patientResponseEntry); - - _fhirTransactionExecutor.ExecuteTransactionAsync( - Arg.Any(), - Arg.Any()) - .Returns(call => - { - // Make sure the request bundle is correct. - Bundle requestBundle = call.ArgAt(0); - - var expectedEntries = new Bundle.EntryComponent[] - { - new Bundle.EntryComponent() - { - FullUrl = imagingStudyRequest.ResourceId.ToString(), - Request = imagingStudyRequest.Request, - Resource = imagingStudyRequest.Resource, - }, - new Bundle.EntryComponent() - { - FullUrl = "Patient/p1", - Request = patientRequest.Request, - Resource = patientRequest.Resource, - }, - }; - - Assert.True( - requestBundle.Entry.Matches(expectedEntries)); - - return responseBundle; - }); - - // Process - await _fhirTransactionPipeline.ProcessAsync(ChangeFeedGenerator.Generate(), CancellationToken.None); - - // The response should have been processed. - Assert.NotNull(_capturedFhirTransactionContext); - - FhirTransactionResponseEntry endpointResponse = _capturedFhirTransactionContext.Response.Endpoint; - - FhirTransactionResponseEntry imaingStudyResponse = _capturedFhirTransactionContext.Response.ImagingStudy; - - Assert.NotNull(imaingStudyResponse); - Assert.Same(imaingStudyResponse.Response, imagingStudyResponseEntry.Response); - Assert.Same(imaingStudyResponse.Resource, imagingStudyResponseEntry.Resource); - - Assert.Null(endpointResponse); - - FhirTransactionResponseEntry patientResponse = _capturedFhirTransactionContext.Response.Patient; - - Assert.NotNull(patientResponse); - Assert.Same(patientResponse.Response, patientResponseEntry.Response); - Assert.Same(patientResponse.Resource, patientResponseEntry.Resource); - } - - [Fact] - public async Task GivenRetryableException_WhenProcessed_ThenItShouldRetry() - { - await ExecuteAndValidateRetryThenThrowTimeOut(new RetryableException()); - } - - [Fact] - public async Task GivenNotConflictException_WhenProcessed_ThenItShouldNotRetry() - { - await ExecuteAndValidate(new Exception(), 1); - } - - [Fact] - public async Task GivenHttpRequestExceptionException_ProcessAsync_ShouldRetryRetryableException() - { - await ExecuteAndValidateRetryThenThrowTimeOut(new HttpRequestException()); - } - - [Fact] - public async Task GivenTaskCancelledExceptionException_ProcessAsync_ShouldRetryRetryableException() - { - await ExecuteAndValidateRetryThenThrowTimeOut(new TaskCanceledException()); - } - - private async Task ExecuteAndValidate(Exception ex, int expectedNumberOfCalls) - { - ChangeFeedEntry changeFeedEntry = ChangeFeedGenerator.Generate(); - var context = new FhirTransactionContext(changeFeedEntry); - - _captureFhirTransactionContextStep.PrepareRequestAsync(Arg.Any(), default).ThrowsForAnyArgs(ex); - - await Assert.ThrowsAsync(ex.GetType(), () => _fhirTransactionPipeline.ProcessAsync(changeFeedEntry, default)); - - await _captureFhirTransactionContextStep.Received(expectedNumberOfCalls).PrepareRequestAsync(Arg.Any(), Arg.Any()); - } - - private async Task ExecuteAndValidateRetryThenThrowTimeOut(Exception ex) - { - ChangeFeedEntry changeFeedEntry = ChangeFeedGenerator.Generate(); - var context = new FhirTransactionContext(changeFeedEntry); - - _captureFhirTransactionContextStep.PrepareRequestAsync(Arg.Any(), Arg.Any()).ThrowsForAnyArgs(ex); - - await Assert.ThrowsAsync(() => _fhirTransactionPipeline.ProcessAsync(changeFeedEntry, default)); - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/FhirTransactionRequestEntryGenerator.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/FhirTransactionRequestEntryGenerator.cs deleted file mode 100644 index bc6ee37d10..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/FhirTransactionRequestEntryGenerator.cs +++ /dev/null @@ -1,48 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Hl7.Fhir.Model; -using Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -namespace Microsoft.Health.DicomCast.Core.UnitTests.Features.Worker.FhirTransaction; - -public static class FhirTransactionRequestEntryGenerator -{ - public static FhirTransactionRequestEntry GenerateDefaultCreateRequestEntry() - where TResource : Resource, new() - { - return new FhirTransactionRequestEntry( - FhirTransactionRequestMode.Create, - new Bundle.RequestComponent() - { - Method = Bundle.HTTPVerb.POST, - }, - new ClientResourceId(), - new TResource()); - } - - public static FhirTransactionRequestEntry GenerateDefaultUpdateRequestEntry(ServerResourceId resourceId) - where TResource : Resource, new() - { - return new FhirTransactionRequestEntry( - FhirTransactionRequestMode.Update, - new Bundle.RequestComponent() - { - Method = Bundle.HTTPVerb.PUT, - }, - resourceId, - new TResource()); - } - - public static FhirTransactionRequestEntry GenerateDefaultNoChangeRequestEntry(ServerResourceId resourceId) - where TResource : Resource, new() - { - return new FhirTransactionRequestEntry( - FhirTransactionRequestMode.None, - request: null, - resourceId, - new TResource()); - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/FhirTransactionRequestResponsePropertyAccessorTests.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/FhirTransactionRequestResponsePropertyAccessorTests.cs deleted file mode 100644 index a688867ff7..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/FhirTransactionRequestResponsePropertyAccessorTests.cs +++ /dev/null @@ -1,233 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using Hl7.Fhir.Model; -using Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; -using Xunit; - -namespace Microsoft.Health.DicomCast.Core.UnitTests.Features.Worker.FhirTransaction; - -public class FhirTransactionRequestResponsePropertyAccessorTests -{ - private readonly FhirTransactionRequest _fhirTransactionRequest = new FhirTransactionRequest(); - private readonly FhirTransactionResponse _fhirTransactionResponse = new FhirTransactionResponse(); - - private readonly FhirTransactionRequestResponsePropertyAccessor _patientPropertyAccessor; - - public FhirTransactionRequestResponsePropertyAccessorTests() - { - _patientPropertyAccessor = CreatePropertyAccessor(); - } - - [Fact] - public void GiveTheRequestEntryGetter_WhenInvoked_ThenCorrectValueShouldBeReturned() - { - FhirTransactionRequestEntry requestEntry = FhirTransactionRequestEntryGenerator.GenerateDefaultCreateRequestEntry(); - - _fhirTransactionRequest.Patient = requestEntry; - - Assert.Same( - requestEntry, - _patientPropertyAccessor.RequestEntryGetter(_fhirTransactionRequest).Single()); - } - - [Fact] - public void GiveTheResponseEntryGetter_WhenInvoked_ThenCorrectValueShouldBeSet() - { - FhirTransactionResponseEntry responseEntry = new(new Bundle.ResponseComponent(), new Patient()); - var responseEntryList = new List { responseEntry }; - - _patientPropertyAccessor.ResponseEntrySetter(_fhirTransactionResponse, responseEntryList); - - Assert.Same( - responseEntry, - _fhirTransactionResponse.Patient); - } - - [Fact] - public void GivenSamePropertyAccessor_WhenHashCodeIsComputed_ThenHashCodeShouldBeTheSame() - { - FhirTransactionRequestResponsePropertyAccessor anotherPatientPropertyAccessor = CreatePropertyAccessor(); - - Assert.Equal(_patientPropertyAccessor.GetHashCode(), anotherPatientPropertyAccessor.GetHashCode()); - } - - [Fact] - public void GivenPropertyAccessorWithDifferentPropertyName_WhenHashCodeIsComputed_ThenHashCodeShouldBeDifferent() - { - FhirTransactionRequestResponsePropertyAccessor anotherPatientPropertyAccessor = CreatePropertyAccessor( - propertyName: "ImagingStudy"); - - Assert.NotEqual(_patientPropertyAccessor.GetHashCode(), anotherPatientPropertyAccessor.GetHashCode()); - } - - [Fact] - public void GivenPropertyAccessorWithDifferentRequestEntryGetter_WhenHashCodeIsComputed_ThenHashCodeShouldBeDifferent() - { - FhirTransactionRequestResponsePropertyAccessor anotherPatientPropertyAccessor = CreatePropertyAccessor( - requestEntryGetter: request => new[] { request.ImagingStudy }); - - Assert.NotEqual(_patientPropertyAccessor.GetHashCode(), anotherPatientPropertyAccessor.GetHashCode()); - } - - [Fact] - public void GivenPropertyAccessorWithDifferentResponseEntrySetter_WhenHashCodeIsComputed_ThenHashCodeShouldBeDifferent() - { - FhirTransactionRequestResponsePropertyAccessor anotherPatientPropertyAccessor = CreatePropertyAccessor( - responseEntrySetter: (response, responseEntry) => response.ImagingStudy = responseEntry.Single()); - - Assert.NotEqual(_patientPropertyAccessor.GetHashCode(), anotherPatientPropertyAccessor.GetHashCode()); - } - - [Fact] - public void GivenAPropertyAccessor_WhenCheckingEqualToDefaultUsingObjectEquals_ThenFalseShouldBeReturned() - { - Assert.False(_patientPropertyAccessor.Equals((object)default)); - } - - [Fact] - public void GivenAPropertyAccessor_WhenCheckingEqualToSamePropertyAccessorUsingObjectEquals_ThenTrueShouldBeReturned() - { - Assert.True(_patientPropertyAccessor.Equals((object)_patientPropertyAccessor)); - } - - [Fact] - public void GivenAPropertyAccessor_WhenCheckingEqualToDifferentPropertyAccessorWhenPropertyNameIsDifferentUsingObjectEquals_ThenFalseShouldBeReturned() - { - Assert.False(_patientPropertyAccessor.Equals( - (object)CreatePropertyAccessor(propertyName: "ImagingStudy"))); - } - - [Fact] - public void GivenAPropertyAccessor_WhenCheckingEqualToDifferentPropertyAccessorWhenRequestEntryGetterIsDifferentUsingObjectEquals_ThenFalseShouldBeReturned() - { - Assert.False(_patientPropertyAccessor.Equals( - (object)CreatePropertyAccessor(requestEntryGetter: request => new[] { request.ImagingStudy }))); - } - - [Fact] - public void GivenAPropertyAccessor_WhenCheckingEqualToDifferentPropertyAccessorWhenResponseEntrySetterIsDifferentUsingObjectEquals_ThenFalseShouldBeReturned() - { - Assert.False(_patientPropertyAccessor.Equals( - (object)CreatePropertyAccessor(responseEntrySetter: (response, responseEntry) => response.ImagingStudy = responseEntry.Single()))); - } - - [Fact] - public void GivenAPropertyAccessor_WhenCheckingEqualToDefaultUsingEquatableEquals_ThenFalseShouldBeReturned() - { - Assert.False(_patientPropertyAccessor.Equals(default)); - } - - [Fact] - public void GivenAPropertyAccessor_WhenCheckingEqualToSamePropertyAccessorUsingEquatableEquals_ThenTrueShouldBeReturned() - { - Assert.True(_patientPropertyAccessor.Equals(_patientPropertyAccessor)); - } - - [Fact] - public void GivenAPropertyAccessor_WhenCheckingEqualToDifferentPropertyAccessorWhenPropertyNameIsDifferentUsingEquatableEquals_ThenFalseShouldBeReturned() - { - Assert.False(_patientPropertyAccessor.Equals( - CreatePropertyAccessor(propertyName: "ImagingStudy"))); - } - - [Fact] - public void GivenAPropertyAccessor_WhenCheckingEqualToDifferentPropertyAccessorWhenRequestEntryGetterIsDifferentUsingEquatableEquals_ThenFalseShouldBeReturned() - { - Assert.False(_patientPropertyAccessor.Equals( - CreatePropertyAccessor(requestEntryGetter: request => new[] { request.ImagingStudy }))); - } - - [Fact] - public void GivenAPropertyAccessor_WhenCheckingEqualToDifferentPropertyAccessorWhenResponseEntrySetterIsDifferentUsingEquatableEquals_ThenFalseShouldBeReturned() - { - Assert.False(_patientPropertyAccessor.Equals( - CreatePropertyAccessor(responseEntrySetter: (response, responseEntry) => response.ImagingStudy = responseEntry.Single()))); - } - - [Fact] - public void GivenAPropertyAccessor_WhenCheckingEqualToDefaultUsingEqualityOperator_ThenFalseShouldBeReturned() - { - Assert.False(_patientPropertyAccessor == default); - } - - [Fact] - public void GivenAPropertyAccessor_WhenCheckingEqualToSamePropertyAccessorUsingEqualityOperator_ThenTrueShouldBeReturned() - { -#pragma warning disable CS1718 // Comparison made to same variable - Assert.True(_patientPropertyAccessor == _patientPropertyAccessor); -#pragma warning restore CS1718 // Comparison made to same variable - } - - [Fact] - public void GivenAPropertyAccessor_WhenCheckingEqualToDifferentPropertyAccessorWhenPropertyNameIsDifferentUsingEqualityOperator_ThenFalseShouldBeReturned() - { - Assert.False(_patientPropertyAccessor == - CreatePropertyAccessor(propertyName: "ImagingStudy")); - } - - [Fact] - public void GivenAPropertyAccessor_WhenCheckingEqualToDifferentPropertyAccessorWhenRequestEntryGetterIsDifferentUsingEqualityOperator_ThenFalseShouldBeReturned() - { - Assert.False(_patientPropertyAccessor == - CreatePropertyAccessor(requestEntryGetter: request => new[] { request.ImagingStudy })); - } - - [Fact] - public void GivenAPropertyAccessor_WhenCheckingEqualToDifferentPropertyAccessorWhenResponseEntrySetterIsDifferentUsingEqualityOperator_ThenFalseShouldBeReturned() - { - Assert.False(_patientPropertyAccessor == - CreatePropertyAccessor(responseEntrySetter: (response, responseEntry) => response.ImagingStudy = responseEntry.Single())); - } - - [Fact] - public void GivenAPropertyAccessor_WhenCheckingEqualToDefaultUsingInequalityOperator_ThenFalseShouldBeReturned() - { - Assert.True(_patientPropertyAccessor != default); - } - - [Fact] - public void GivenAPropertyAccessor_WhenCheckingEqualToSamePropertyAccessorUsingInequalityOperator_ThenTrueShouldBeReturned() - { -#pragma warning disable CS1718 // Comparison made to same variable - Assert.False(_patientPropertyAccessor != _patientPropertyAccessor); -#pragma warning restore CS1718 // Comparison made to same variable - } - - [Fact] - public void GivenAPropertyAccessor_WhenCheckingEqualToDifferentPropertyAccessorWhenPropertyNameIsDifferentUsingInequalityOperator_ThenFalseShouldBeReturned() - { - Assert.True(_patientPropertyAccessor != - CreatePropertyAccessor(propertyName: "ImagingStudy")); - } - - [Fact] - public void GivenAPropertyAccessor_WhenCheckingEqualToDifferentPropertyAccessorWhenRequestEntryGetterIsDifferentUsingInequalityOperator_ThenFalseShouldBeReturned() - { - Assert.True(_patientPropertyAccessor != - CreatePropertyAccessor(requestEntryGetter: request => new[] { request.ImagingStudy })); - } - - [Fact] - public void GivenAPropertyAccessor_WhenCheckingEqualToDifferentPropertyAccessorWhenResponseEntrySetterIsDifferentUsingInequalityOperator_ThenFalseShouldBeReturned() - { - Assert.True(_patientPropertyAccessor != - CreatePropertyAccessor(responseEntrySetter: (response, responseEntry) => response.ImagingStudy = responseEntry.Single())); - } - - private static FhirTransactionRequestResponsePropertyAccessor CreatePropertyAccessor( - string propertyName = "Patient", - Func> requestEntryGetter = null, - Action> responseEntrySetter = null) - { - requestEntryGetter ??= request => new List { request.Patient }; - responseEntrySetter ??= (response, responseEntry) => response.Patient = responseEntry.Single(); - - return new FhirTransactionRequestResponsePropertyAccessor(propertyName, requestEntryGetter, responseEntrySetter); - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/FhirTransactionRequestResponsePropertyAccessorsTests.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/FhirTransactionRequestResponsePropertyAccessorsTests.cs deleted file mode 100644 index 36effd40ee..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/FhirTransactionRequestResponsePropertyAccessorsTests.cs +++ /dev/null @@ -1,94 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Linq; -using Hl7.Fhir.Model; -using Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; -using Xunit; - -namespace Microsoft.Health.DicomCast.Core.UnitTests.Features.Worker.FhirTransaction; - -public class FhirTransactionRequestResponsePropertyAccessorsTests -{ - private readonly FhirTransactionRequestResponsePropertyAccessors _transactionRequestResponsePropertyAccessors = new FhirTransactionRequestResponsePropertyAccessors(); - - private readonly FhirTransactionRequest _fhirTransactionRequest = new FhirTransactionRequest() - { - Patient = FhirTransactionRequestEntryGenerator.GenerateDefaultCreateRequestEntry(), - ImagingStudy = FhirTransactionRequestEntryGenerator.GenerateDefaultUpdateRequestEntry(new ServerResourceId(ResourceType.ImagingStudy, "123")), - Endpoint = FhirTransactionRequestEntryGenerator.GenerateDefaultNoChangeRequestEntry(new ServerResourceId(ResourceType.Endpoint, "abc")), - }; - - private readonly FhirTransactionResponse _fhirTransactionResponse = new FhirTransactionResponse(); - - [Fact] - public void GivenAPatientRequest_WhenPropertyGetterIsUsed_ThenCorrectValueShouldBeReturned() - { - ExecuteAndValidatePropertyGetter(nameof(FhirTransactionRequest.Patient), _fhirTransactionRequest.Patient); - } - - [Fact] - public void GivenAPatientResponse_WhenPropertySetterIsUsed_ThenCorrectValueShouldBeSet() - { - IEnumerable expectedResponse = ExecutePropertySetter(nameof(FhirTransactionRequest.Patient)); - - Assert.Same(_fhirTransactionResponse.Patient, expectedResponse.Single()); - Assert.Null(_fhirTransactionResponse.ImagingStudy); - Assert.Null(_fhirTransactionResponse.Endpoint); - Assert.Null(_fhirTransactionResponse.Observation); - } - - [Fact] - public void GivenAnImagingStudyRequest_WhenPropertyGetterIsUsed_ThenCorrectValueShouldBeReturned() - { - ExecuteAndValidatePropertyGetter(nameof(FhirTransactionRequest.ImagingStudy), _fhirTransactionRequest.ImagingStudy); - } - - [Fact] - public void GivenAnImagingStudyResponse_WhenPropertySetterIsUsed_ThenCorrectValueShouldBeSet() - { - IEnumerable expectedResponse = ExecutePropertySetter(nameof(FhirTransactionRequest.ImagingStudy)); - - Assert.Same(_fhirTransactionResponse.ImagingStudy, expectedResponse.Single()); - } - - [Fact] - public void GivenAnEndpointRequest_WhenPropertyGetterIsUsed_ThenCorrectValueShouldBeReturned() - { - ExecuteAndValidatePropertyGetter(nameof(FhirTransactionRequest.Endpoint), _fhirTransactionRequest.Endpoint); - } - - [Fact] - public void GivenAnEndpointResponse_WhenPropertySetterIsUsed_ThenCorrectValueShouldBeSet() - { - IEnumerable expectedResponse = ExecutePropertySetter(nameof(FhirTransactionRequest.Endpoint)); - - Assert.Same(_fhirTransactionResponse.Endpoint, expectedResponse.Single()); - } - - private void ExecuteAndValidatePropertyGetter(string propertyName, FhirTransactionRequestEntry expectedEntry) - { - FhirTransactionRequestResponsePropertyAccessor propertyAccessor = GetPropertyAccessor(propertyName); - - IEnumerable requestEntry = propertyAccessor.RequestEntryGetter(_fhirTransactionRequest); - - Assert.Same(expectedEntry, requestEntry.Single()); - } - - private IEnumerable ExecutePropertySetter(string propertyName) - { - FhirTransactionRequestResponsePropertyAccessor propertyAccessor = GetPropertyAccessor(propertyName); - - var expectedResponse = new[] { new FhirTransactionResponseEntry(new Bundle.ResponseComponent(), new Patient()) }; - - propertyAccessor.ResponseEntrySetter(_fhirTransactionResponse, expectedResponse); - - return expectedResponse; - } - - private FhirTransactionRequestResponsePropertyAccessor GetPropertyAccessor(string propertyName) - => _transactionRequestResponsePropertyAccessors.PropertyAccessors.First(propertyAccessor => propertyAccessor.PropertyName == propertyName); -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/ImagingStudy/ImagingStudyDeleteHandlerTests.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/ImagingStudy/ImagingStudyDeleteHandlerTests.cs deleted file mode 100644 index 13e50727cf..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/ImagingStudy/ImagingStudyDeleteHandlerTests.cs +++ /dev/null @@ -1,182 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Hl7.Fhir.Model; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Client.Models; -using Microsoft.Health.DicomCast.Core.Configurations; -using Microsoft.Health.DicomCast.Core.Features.Fhir; -using Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; -using NSubstitute; -using Xunit; -using Task = System.Threading.Tasks.Task; - -namespace Microsoft.Health.DicomCast.Core.UnitTests.Features.Worker.FhirTransaction; - -public class ImagingStudyDeleteHandlerTests -{ - private const string DefaultDicomWebEndpoint = "https://dicom/"; - - private readonly IFhirService _fhirService; - private readonly ImagingStudyDeleteHandler _imagingStudyDeleteHandler; - private readonly DicomWebConfiguration _configuration; - - private FhirTransactionContext _fhirTransactionContext; - - public ImagingStudyDeleteHandlerTests() - { - _configuration = new DicomWebConfiguration() { Endpoint = new System.Uri(DefaultDicomWebEndpoint), }; - IOptions optionsConfiguration = Options.Create(_configuration); - - _fhirService = Substitute.For(); - _imagingStudyDeleteHandler = new ImagingStudyDeleteHandler(_fhirService, optionsConfiguration); - } - - [Fact] - public async Task GivenAChangeFeedEntryToDeleteAnInstanceWithinASeriesContainingMoreThanOneInstance_WhenBuilt_ThenCorrectEntryComponentShouldBeCreated() - { - const string studyInstanceUid = "1"; - const string seriesInstanceUid = "2"; - const string sopInstanceUid = "3"; - const string sopInstanceUid1 = "3"; - const string patientResourceId = "p1"; - - // create a new ImagingStudy - ImagingStudy imagingStudy = FhirResourceBuilder.CreateNewImagingStudy(studyInstanceUid, new List() { seriesInstanceUid }, new List() { sopInstanceUid, sopInstanceUid1 }, patientResourceId); - _fhirService.RetrieveImagingStudyAsync(Arg.Any(), Arg.Any()).Returns(imagingStudy); - - // delete an existing instance within a study - FhirTransactionRequestEntry entry = await BuildImagingStudyEntryComponent(studyInstanceUid, seriesInstanceUid, sopInstanceUid, patientResourceId); - - ImagingStudy updatedImagingStudy = ValidationUtility.ValidateImagingStudyUpdate(studyInstanceUid, patientResourceId, entry, hasAccessionNumber: false); - - Assert.Equal(ImagingStudy.ImagingStudyStatus.Available, updatedImagingStudy.Status); - - Assert.Collection( - updatedImagingStudy.Series, - series => - { - Assert.Equal(seriesInstanceUid, series.Uid); - - Assert.Collection( - series.Instance, - instance => Assert.Equal(sopInstanceUid1, instance.Uid)); - }); - } - - [Fact] - public async Task GivenAChangeFeedEntryToDeleteAnInstanceWithinASeriesContainingOneInstanceDifferentSouce_WhenBuilt_ShouldUpdateNotDelete() - { - const string studyInstanceUid = "1"; - const string seriesInstanceUid = "2"; - const string sopInstanceUid = "3"; - const string patientResourceId = "p1"; - - // create a new ImagingStudy - ImagingStudy imagingStudy = FhirResourceBuilder.CreateNewImagingStudy(studyInstanceUid, new List() { seriesInstanceUid }, new List() { sopInstanceUid }, patientResourceId); - _fhirService.RetrieveImagingStudyAsync(Arg.Any(), Arg.Any()).Returns(imagingStudy); - - // delete an existing instance within a study - FhirTransactionRequestEntry entry = await BuildImagingStudyEntryComponent(studyInstanceUid, seriesInstanceUid, sopInstanceUid, patientResourceId); - - Assert.Equal(FhirTransactionRequestMode.Update, entry.RequestMode); - } - - [Fact] - public async Task GivenAChangeFeedEntryToDeleteAnInstanceWithinASeriesContainingOneInstanceSameSouce_WhenBuilt_ShouldDelete() - { - const string studyInstanceUid = "1"; - const string seriesInstanceUid = "2"; - const string sopInstanceUid = "3"; - const string patientResourceId = "p1"; - - // create a new ImagingStudy - ImagingStudy imagingStudy = FhirResourceBuilder.CreateNewImagingStudy(studyInstanceUid, new List() { seriesInstanceUid }, new List() { sopInstanceUid }, patientResourceId, DefaultDicomWebEndpoint); - _fhirService.RetrieveImagingStudyAsync(Arg.Any(), Arg.Any()).Returns(imagingStudy); - - // delete an existing instance within a study - FhirTransactionRequestEntry entry = await BuildImagingStudyEntryComponent(studyInstanceUid, seriesInstanceUid, sopInstanceUid, patientResourceId); - - Assert.Equal(FhirTransactionRequestMode.Delete, entry.RequestMode); - } - - [Fact] - public async Task GivenAChangeFeedEntryToDeleteAnInstanceWithinAStudyContainingMoreThanOneSeries_WhenBuilt_ThenCorrectEntryComponentShouldBeCreated() - { - const string studyInstanceUid = "1"; - const string seriesInstanceUid = "2"; - const string seriesInstanceUid1 = "3"; - const string sopInstanceUid = "3"; - const string patientResourceId = "p1"; - - // create a new ImagingStudy - ImagingStudy imagingStudy = FhirResourceBuilder.CreateNewImagingStudy(studyInstanceUid, new List() { seriesInstanceUid, seriesInstanceUid1 }, new List() { sopInstanceUid, }, patientResourceId); - _fhirService.RetrieveImagingStudyAsync(Arg.Any(), Arg.Any()).Returns(imagingStudy); - - // delete an existing instance within a study - FhirTransactionRequestEntry entry = await BuildImagingStudyEntryComponent(studyInstanceUid, seriesInstanceUid, sopInstanceUid, patientResourceId); - - ImagingStudy updatedImagingStudy = ValidationUtility.ValidateImagingStudyUpdate(studyInstanceUid, patientResourceId, entry, hasAccessionNumber: false); - - Assert.Equal(ImagingStudy.ImagingStudyStatus.Available, updatedImagingStudy.Status); - - Assert.Collection( - updatedImagingStudy.Series, - series => - { - ValidationUtility.ValidateSeries(series, seriesInstanceUid1, sopInstanceUid); - }); - } - - [Fact] - public async Task GivenAChangeFeedEntryForDeleteInstanceThatDoesNotExistsWithinGivenStudy_WhenBuilt_ThenNoEntryComponentShouldBeCreated() - { - const string studyInstanceUid = "1"; - const string seriesInstanceUid = "2"; - const string sopInstanceUid = "3"; - const string sopInstanceUid1 = "4"; - const string patientResourceId = "p1"; - - // create a new ImagingStudy - ImagingStudy imagingStudy = FhirResourceBuilder.CreateNewImagingStudy(studyInstanceUid, new List() { seriesInstanceUid }, new List() { sopInstanceUid }, patientResourceId); - _fhirService.RetrieveImagingStudyAsync(Arg.Any(), Arg.Any()).Returns(imagingStudy); - - // try delete non-existing instance within a study - FhirTransactionRequestEntry entry = await BuildImagingStudyEntryComponent(studyInstanceUid, seriesInstanceUid, sopInstanceUid1, patientResourceId); - - Assert.Null(entry); - } - - [Fact] - public async Task GivenAChangeFeedEntryForDeleteForStudyInstanceThatDoesNotExists_WhenBuilt_ThenNoEntryComponentShouldBeCreated() - { - const string studyInstanceUid = "1"; - const string seriesInstanceUid = "2"; - const string sopInstanceUid = "3"; - - // try delete instance from a non-existing study - FhirTransactionRequestEntry entry = await BuildImagingStudyEntryComponent(studyInstanceUid, seriesInstanceUid, sopInstanceUid, patientResourceId: "p1"); - - Assert.Null(entry); - } - - private async Task BuildImagingStudyEntryComponent(string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid, string patientResourceId) - { - ChangeFeedEntry changeFeedEntry = ChangeFeedGenerator.Generate(action: ChangeFeedAction.Delete, studyInstanceUid: studyInstanceUid, seriesInstanceUid: seriesInstanceUid, sopInstanceUid: sopInstanceUid); - return await PrepareRequestAsync(changeFeedEntry, patientResourceId); - } - - private async Task PrepareRequestAsync(ChangeFeedEntry changeFeedEntry, string patientResourceId) - { - _fhirTransactionContext = new FhirTransactionContext(changeFeedEntry); - - _fhirTransactionContext.Request.Patient = FhirTransactionRequestEntryGenerator.GenerateDefaultNoChangeRequestEntry(new ServerResourceId(ResourceType.Patient, patientResourceId)); - - return await _imagingStudyDeleteHandler.BuildAsync(_fhirTransactionContext, CancellationToken.None); - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/ImagingStudy/ImagingStudyInstancePropertySynchronizerTests.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/ImagingStudy/ImagingStudyInstancePropertySynchronizerTests.cs deleted file mode 100644 index 08df0018e1..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/ImagingStudy/ImagingStudyInstancePropertySynchronizerTests.cs +++ /dev/null @@ -1,101 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using FellowOakDicom; -using Hl7.Fhir.Model; -using Microsoft.Extensions.Options; -using Microsoft.Health.DicomCast.Core.Configurations; -using Microsoft.Health.DicomCast.Core.Features.ExceptionStorage; -using Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; -using NSubstitute; -using Xunit; -using Task = System.Threading.Tasks.Task; - -namespace Microsoft.Health.DicomCast.Core.UnitTests.Features.Worker.FhirTransaction; - -public class ImagingStudyInstancePropertySynchronizerTests -{ - private const string StudyInstanceUid = "111"; - private const string SeriesInstanceUid = "222"; - private const string SopInstanceUid = "333"; - private const string SopClassUid = "4444"; - private const string PatientResourceId = "555"; - - private static readonly CancellationToken DefaultCancellationToken = new CancellationTokenSource().Token; - private readonly IImagingStudyInstancePropertySynchronizer _imagingStudyInstancePropertySynchronizer; - private readonly DicomCastConfiguration _dicomCastConfig = new DicomCastConfiguration(); - private readonly IExceptionStore _exceptionStore = Substitute.For(); - - public ImagingStudyInstancePropertySynchronizerTests() - { - _imagingStudyInstancePropertySynchronizer = new ImagingStudyInstancePropertySynchronizer(Options.Create(_dicomCastConfig), _exceptionStore); - } - - [Fact] - public async Task GivenATransactionContexAndImagingStudy_WhenprocessedForInstance_ThenDicomPropertiesAreCorrectlyMappedtoInstanceWithinImagingStudyAsync() - { - DicomDataset dataset = FhirTransactionContextBuilder.CreateDicomDataset(); - - ImagingStudy imagingStudy = FhirResourceBuilder.CreateNewImagingStudy(StudyInstanceUid, new List() { SeriesInstanceUid }, new List() { SopInstanceUid }, PatientResourceId); - FhirTransactionContext context = FhirTransactionContextBuilder.DefaultFhirTransactionContext(dataset); - - if (imagingStudy.Series.Count() > 0) - { - System.Console.WriteLine(""); - } - ImagingStudy.SeriesComponent series = imagingStudy.Series.First(); - ImagingStudy.InstanceComponent instance = series.Instance.First(); - - await _imagingStudyInstancePropertySynchronizer.SynchronizeAsync(context, instance, DefaultCancellationToken); - - Assert.Equal(SopClassUid, instance.SopClass.Code); - Assert.Equal(1, instance.Number); - } - - [Fact] - public async Task GivenATransactionContextWithUpdatedInstanceNumber_WhenprocessedForInstance_ThenDicomPropertyValuesAreUpdatedCorrectlyAsync() - { - DicomDataset dataset = FhirTransactionContextBuilder.CreateDicomDataset(); - - ImagingStudy imagingStudy = FhirResourceBuilder.CreateNewImagingStudy(StudyInstanceUid, new List() { SeriesInstanceUid }, new List() { SopInstanceUid }, PatientResourceId); - FhirTransactionContext context = FhirTransactionContextBuilder.DefaultFhirTransactionContext(dataset); - - ImagingStudy.SeriesComponent series = imagingStudy.Series.First(); - ImagingStudy.InstanceComponent instance = series.Instance.First(); - - await _imagingStudyInstancePropertySynchronizer.SynchronizeAsync(context, instance, DefaultCancellationToken); - - Assert.Equal(1, instance.Number); - - FhirTransactionContext newContext = FhirTransactionContextBuilder.DefaultFhirTransactionContext(FhirTransactionContextBuilder.CreateDicomDataset(instanceNumber: "2")); - - await _imagingStudyInstancePropertySynchronizer.SynchronizeAsync(newContext, instance, DefaultCancellationToken); - Assert.Equal(2, instance.Number); - } - - [Fact] - public async Task GivenATransactionContextWithNoDicomPropertyValueChange_WhenprocessedForInstancee_ThenDicomPropertyValuesUpdateIsSkippedAsync() - { - DicomDataset dataset = FhirTransactionContextBuilder.CreateDicomDataset(); - - ImagingStudy imagingStudy = FhirResourceBuilder.CreateNewImagingStudy(StudyInstanceUid, new List() { SeriesInstanceUid }, new List() { SopInstanceUid }, PatientResourceId); - FhirTransactionContext context = FhirTransactionContextBuilder.DefaultFhirTransactionContext(dataset); - - ImagingStudy.SeriesComponent series = imagingStudy.Series.First(); - ImagingStudy.InstanceComponent instance = series.Instance.First(); - - await _imagingStudyInstancePropertySynchronizer.SynchronizeAsync(context, instance, DefaultCancellationToken); - - Assert.Equal(1, instance.Number); - - FhirTransactionContext newContext = FhirTransactionContextBuilder.DefaultFhirTransactionContext(dataset); - - await _imagingStudyInstancePropertySynchronizer.SynchronizeAsync(newContext, instance, DefaultCancellationToken); - Assert.Equal(1, instance.Number); - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/ImagingStudy/ImagingStudyPipelineHelperTests.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/ImagingStudy/ImagingStudyPipelineHelperTests.cs deleted file mode 100644 index 88199799b1..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/ImagingStudy/ImagingStudyPipelineHelperTests.cs +++ /dev/null @@ -1,100 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using FellowOakDicom; -using Hl7.Fhir.Model; -using Microsoft.Health.DicomCast.Core.Features.ExceptionStorage; -using Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; -using NSubstitute; -using Xunit; -using Task = System.Threading.Tasks.Task; - -namespace Microsoft.Health.DicomCast.Core.UnitTests.Features.Worker.FhirTransaction; - -public class ImagingStudyPipelineHelperTests -{ - private const string DefaultStudyInstanceUid = "111"; - private const string DefaultSeriesInstanceUid = "222"; - private const string DefaultSopInstanceUid = "333"; - private const string DefaultPatientResourceId = "555"; - - private readonly IExceptionStore _exceptionStore = Substitute.For(); - - [Fact] - public void GivenAChangeFeedEntryWithInvalidUtcTimeOffset_WhenDateTimeOffsetIsCalculated_ThenInvalidDicomTagValueExceptionIsThrown() - { - FhirTransactionContext fhirTransactionContext = FhirTransactionContextBuilder.DefaultFhirTransactionContext(); - fhirTransactionContext.ChangeFeedEntry.Metadata.Add(DicomTag.TimezoneOffsetFromUTC, "0"); - - Assert.Throws( - () => ImagingStudyPipelineHelper.SetDateTimeOffSet(fhirTransactionContext)); - } - - [Theory] - [InlineData(14, 0, "+1400")] - [InlineData(-8, 0, "-0800")] - [InlineData(-14, 0, "-1400")] - [InlineData(8, 0, "+0800")] - [InlineData(0, 0, "+0000")] - [InlineData(8, 30, "+0830")] - public void GivenAChangeFeedEntry_WhenDateTimeOffsetIsCalculated_ThenDateTimeOffsetIsSet(int hour, int minute, string dicomValue) - { - FhirTransactionContext fhirTransactionContext = FhirTransactionContextBuilder.DefaultFhirTransactionContext(); - - DateTimeOffset utcTimeZoneOffset = new DateTimeOffset(2020, 1, 1, 0, 0, 0, new TimeSpan(hour, minute, 0)); - - fhirTransactionContext.ChangeFeedEntry.Metadata.Add(DicomTag.TimezoneOffsetFromUTC, dicomValue); - - ImagingStudyPipelineHelper.SetDateTimeOffSet(fhirTransactionContext); - - Assert.Equal(utcTimeZoneOffset.Offset, fhirTransactionContext.UtcDateTimeOffset); - } - - [Fact] - public void GivenAccessionNumber_WhenGetAccessionNumber_ThenReturnsCorrectIdentifier() - { - string accessionNumber = "01234"; - var result = ImagingStudyPipelineHelper.GetAccessionNumber(accessionNumber); - ValidationUtility.ValidateAccessionNumber(null, accessionNumber, result); - } - - [Fact] - public async Task SyncPropertiesAsync_PartialValidationNotEnabled_ThrowsError() - { - ImagingStudy imagingStudy = FhirResourceBuilder.CreateNewImagingStudy(DefaultStudyInstanceUid, new List() { DefaultSeriesInstanceUid }, new List() { DefaultSopInstanceUid }, DefaultPatientResourceId); - FhirTransactionContext context = FhirTransactionContextBuilder.DefaultFhirTransactionContext(FhirTransactionContextBuilder.CreateDicomDataset()); - - Action actionSubstitute = Substitute.For>(); - actionSubstitute.When(x => x.Invoke(imagingStudy, context)).Do(x => throw new InvalidDicomTagValueException("invalid tag", "invalid tag")); - - await Assert.ThrowsAsync(() => ImagingStudyPipelineHelper.SynchronizePropertiesAsync(imagingStudy, context, actionSubstitute, false, true, _exceptionStore)); - } - - [Fact] - public async Task SyncPropertiesAsync_PartialValidationEnabledAndPropertyRequired_ThrowsError() - { - ImagingStudy imagingStudy = FhirResourceBuilder.CreateNewImagingStudy(DefaultStudyInstanceUid, new List() { DefaultSeriesInstanceUid }, new List() { DefaultSopInstanceUid }, DefaultPatientResourceId); - FhirTransactionContext context = FhirTransactionContextBuilder.DefaultFhirTransactionContext(FhirTransactionContextBuilder.CreateDicomDataset()); - - Action actionSubstitute = Substitute.For>(); - actionSubstitute.When(x => x.Invoke(imagingStudy, context)).Do(x => throw new InvalidDicomTagValueException("invalid tag", "invalid tag")); - - await Assert.ThrowsAsync(() => ImagingStudyPipelineHelper.SynchronizePropertiesAsync(imagingStudy, context, actionSubstitute, true, true, _exceptionStore)); - } - - [Fact] - public async Task SyncPropertiesAsync_PartialValidationEnabledAndPropertyNotRequired_NoError() - { - ImagingStudy imagingStudy = FhirResourceBuilder.CreateNewImagingStudy(DefaultStudyInstanceUid, new List() { DefaultSeriesInstanceUid }, new List() { DefaultSopInstanceUid }, DefaultPatientResourceId); - FhirTransactionContext context = FhirTransactionContextBuilder.DefaultFhirTransactionContext(FhirTransactionContextBuilder.CreateDicomDataset()); - - Action actionSubstitute = Substitute.For>(); - actionSubstitute.When(x => x.Invoke(imagingStudy, context)).Do(x => throw new InvalidDicomTagValueException("invalid tag", "invalid tag")); - - await ImagingStudyPipelineHelper.SynchronizePropertiesAsync(imagingStudy, context, actionSubstitute, false, false, _exceptionStore); - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/ImagingStudy/ImagingStudyPipelineStepTests.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/ImagingStudy/ImagingStudyPipelineStepTests.cs deleted file mode 100644 index 27d2561a84..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/ImagingStudy/ImagingStudyPipelineStepTests.cs +++ /dev/null @@ -1,94 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Net; -using System.Threading; -using Hl7.Fhir.Model; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Health.Dicom.Client.Models; -using Microsoft.Health.DicomCast.Core.Features.Fhir; -using Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; -using NSubstitute; -using Xunit; -using Task = System.Threading.Tasks.Task; - -namespace Microsoft.Health.DicomCast.Core.UnitTests.Features.Worker.FhirTransaction; - -public class ImagingStudyPipelineStepTests -{ - private static readonly CancellationToken DefaultCancellationToken = new CancellationTokenSource().Token; - - private readonly IImagingStudyDeleteHandler _imagingStudyDeleteHandler; - private readonly IImagingStudyUpsertHandler _imagingStudyUpsertHandler; - private readonly ImagingStudyPipelineStep _imagingStudyPipeline; - - public ImagingStudyPipelineStepTests() - { - _imagingStudyDeleteHandler = Substitute.For(); - _imagingStudyUpsertHandler = Substitute.For(); - - _imagingStudyPipeline = new ImagingStudyPipelineStep(_imagingStudyUpsertHandler, _imagingStudyDeleteHandler, NullLogger.Instance); - } - - [Fact] - public async Task GivenAChangeFeedEntryForCreate_WhenPreparingTheRequest_ThenCreateHandlerIsCalled() - { - const string studyInstanceUid = "1"; - const string seriesInstanceUid = "2"; - const string sopInstanceUid = "3"; - - ChangeFeedEntry changeFeed = ChangeFeedGenerator.Generate( - action: ChangeFeedAction.Create, - studyInstanceUid: studyInstanceUid, - seriesInstanceUid: seriesInstanceUid, - sopInstanceUid: sopInstanceUid); - - var fhirTransactionContext = new FhirTransactionContext(changeFeed); - - await _imagingStudyPipeline.PrepareRequestAsync(fhirTransactionContext, DefaultCancellationToken); - - await _imagingStudyUpsertHandler.Received(1).BuildAsync(fhirTransactionContext, DefaultCancellationToken); - } - - [Fact] - public void GivenARequestToCreateAnImagingStudy_WhenResponseIsOK_ThenResourceConflictExceptionShouldBeThrown() - { - var response = new Bundle.ResponseComponent(); - - response.AddAnnotation(HttpStatusCode.OK); - - var context = new FhirTransactionContext(ChangeFeedGenerator.Generate()); - - context.Request.ImagingStudy = FhirTransactionRequestEntryGenerator.GenerateDefaultCreateRequestEntry(); - - context.Response.ImagingStudy = new FhirTransactionResponseEntry(response, new ImagingStudy()); - - Assert.Throws(() => _imagingStudyPipeline.ProcessResponse(context)); - } - - [Fact] - public async Task GivenAChangeFeedEntryForDelete_WhenBuilt_ThenDeleteHandlerIsCalled() - { - const string studyInstanceUid = "1"; - const string seriesInstanceUid = "2"; - const string sopInstanceUid = "3"; - const string patientResourceId = "p1"; - - ChangeFeedEntry changeFeed = ChangeFeedGenerator.Generate( - action: ChangeFeedAction.Delete, - studyInstanceUid: studyInstanceUid, - seriesInstanceUid: seriesInstanceUid, - sopInstanceUid: sopInstanceUid); - - var fhirTransactionContext = new FhirTransactionContext(changeFeed); - - fhirTransactionContext.Request.Patient = FhirTransactionRequestEntryGenerator.GenerateDefaultNoChangeRequestEntry( - new ServerResourceId(ResourceType.Patient, patientResourceId)); - - await _imagingStudyPipeline.PrepareRequestAsync(fhirTransactionContext, DefaultCancellationToken); - - await _imagingStudyDeleteHandler.Received(1).BuildAsync(fhirTransactionContext, DefaultCancellationToken); - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/ImagingStudy/ImagingStudyPropertySynchronizerTests.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/ImagingStudy/ImagingStudyPropertySynchronizerTests.cs deleted file mode 100644 index 092975dd14..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/ImagingStudy/ImagingStudyPropertySynchronizerTests.cs +++ /dev/null @@ -1,240 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using Hl7.Fhir.Model; -using Microsoft.Extensions.Options; -using Microsoft.Health.DicomCast.Core.Configurations; -using Microsoft.Health.DicomCast.Core.Features.ExceptionStorage; -using Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; -using NSubstitute; -using Xunit; -using Task = System.Threading.Tasks.Task; - -namespace Microsoft.Health.DicomCast.Core.UnitTests.Features.Worker.FhirTransaction; - -public class ImagingStudyPropertySynchronizerTests -{ - private const string DefaultStudyInstanceUid = "111"; - private const string DefaultSeriesInstanceUid = "222"; - private const string DefaultSopInstanceUid = "333"; - private const string DefaultPatientResourceId = "555"; - private const string NewAccessionNumber = "2"; - private readonly IImagingStudyPropertySynchronizer _imagingStudyPropertySynchronizer; - - private readonly DicomCastConfiguration _dicomCastConfig = new DicomCastConfiguration(); - private readonly IExceptionStore _exceptionStore = Substitute.For(); - - public ImagingStudyPropertySynchronizerTests() - { - _imagingStudyPropertySynchronizer = new ImagingStudyPropertySynchronizer(Options.Create(_dicomCastConfig), _exceptionStore); - } - - [Fact] - public async Task GivenATransactionContexAndImagingStudy_WhenProcessedForStudy_ThenDicomPropertiesAreCorrectlyMappedtoImagingStudyAsync() - { - ImagingStudy imagingStudy = FhirResourceBuilder.CreateNewImagingStudy(DefaultStudyInstanceUid, new List() { DefaultSeriesInstanceUid }, new List() { DefaultSopInstanceUid }, DefaultPatientResourceId); - FhirTransactionContext context = FhirTransactionContextBuilder.DefaultFhirTransactionContext(FhirTransactionContextBuilder.CreateDicomDataset()); - - await _imagingStudyPropertySynchronizer.SynchronizeAsync(context, imagingStudy); - - Assert.Collection( - imagingStudy.Endpoint, - reference => string.Equals(reference.Reference, context.Request.Endpoint.Resource.ToString(), StringComparison.Ordinal)); - - Assert.Collection( - imagingStudy.Modality, - modality => string.Equals(modality.Code, "MODALITY", StringComparison.Ordinal)); - - Assert.Collection( - imagingStudy.Note, - note => string.Equals(note.Text.ToString(), "Study Description", StringComparison.Ordinal)); - - Assert.Collection( - imagingStudy.Identifier, - identifier => string.Equals(identifier.Value, $"urn:oid:{DefaultStudyInstanceUid}", StringComparison.Ordinal), // studyinstanceUid - identifier => string.Equals(identifier.Value, "1", StringComparison.Ordinal)); // accession number - - Assert.Equal(new FhirDateTime(1974, 7, 10, 7, 10, 24, TimeSpan.Zero), imagingStudy.StartedElement); - } - - [Fact] - public async Task GivenATransactionContexAndImagingStudyWithNewModality_WhenProcessedForStudy_ThenNewModalityIsAddedAsync() - { - ImagingStudy imagingStudy = FhirResourceBuilder.CreateNewImagingStudy(DefaultStudyInstanceUid, new List() { DefaultSeriesInstanceUid }, new List() { DefaultSopInstanceUid }, DefaultPatientResourceId); - FhirTransactionContext context = FhirTransactionContextBuilder.DefaultFhirTransactionContext(FhirTransactionContextBuilder.CreateDicomDataset()); - - await _imagingStudyPropertySynchronizer.SynchronizeAsync(context, imagingStudy); - - Assert.Collection( - imagingStudy.Modality, - modality => string.Equals(modality.Code, "MODALITY", StringComparison.Ordinal)); - - FhirTransactionContext newConText = FhirTransactionContextBuilder.DefaultFhirTransactionContext(FhirTransactionContextBuilder.CreateDicomDataset(modalityInStudy: "NEWMODALITY", modalityInSeries: "NEWMODALITY")); - - await _imagingStudyPropertySynchronizer.SynchronizeAsync(newConText, imagingStudy); - - Assert.Collection( - imagingStudy.Modality, - modality => string.Equals(modality.Code, "MODALITY", StringComparison.Ordinal), - modality => string.Equals(modality.Code, "NEWMODALITY", StringComparison.Ordinal)); - } - - [Fact] - public async Task GivenATransactionContextAndImagingStudyWithExitsingModality_WhenProcessedForStudy_ThenModalityIsNotAddedAsync() - { - ImagingStudy imagingStudy = FhirResourceBuilder.CreateNewImagingStudy(DefaultStudyInstanceUid, new List() { DefaultSeriesInstanceUid }, new List() { DefaultSopInstanceUid }, DefaultPatientResourceId); - FhirTransactionContext context = FhirTransactionContextBuilder.DefaultFhirTransactionContext(FhirTransactionContextBuilder.CreateDicomDataset()); - - await _imagingStudyPropertySynchronizer.SynchronizeAsync(context, imagingStudy); - - Assert.Collection( - imagingStudy.Modality, - modality => string.Equals(modality.Code, "MODALITY", StringComparison.Ordinal)); - - await _imagingStudyPropertySynchronizer.SynchronizeAsync(context, imagingStudy); - - Assert.Collection( - imagingStudy.Modality, - modality => string.Equals(modality.Code, "MODALITY", StringComparison.Ordinal)); - } - - [Fact] - public async Task GivenATransactionContexAndImagingStudyWithNewAccessionNumber_WhenProcessedForStudy_ThenNewAccessionNumberIsAddedAsync() - { - ImagingStudy imagingStudy = FhirResourceBuilder.CreateNewImagingStudy(DefaultStudyInstanceUid, new List() { DefaultSeriesInstanceUid }, new List() { DefaultSopInstanceUid }, DefaultPatientResourceId); - FhirTransactionContext context = FhirTransactionContextBuilder.DefaultFhirTransactionContext(FhirTransactionContextBuilder.CreateDicomDataset()); - - await _imagingStudyPropertySynchronizer.SynchronizeAsync(context, imagingStudy); - - Assert.Collection( - imagingStudy.Identifier, - identifier => ValidationUtility.ValidateIdentifier("urn:dicom:uid", $"urn:oid:{DefaultStudyInstanceUid}", identifier), // studyinstanceUid - identifier => ValidationUtility.ValidateAccessionNumber(null, FhirTransactionContextBuilder.DefaultAccessionNumber, identifier)); // accession number - - FhirTransactionContext newConText = FhirTransactionContextBuilder.DefaultFhirTransactionContext(FhirTransactionContextBuilder.CreateDicomDataset(accessionNumber: NewAccessionNumber)); - - await _imagingStudyPropertySynchronizer.SynchronizeAsync(newConText, imagingStudy); - - Assert.Collection( - imagingStudy.Identifier, - identifier => ValidationUtility.ValidateIdentifier("urn:dicom:uid", $"urn:oid:{DefaultStudyInstanceUid}", identifier), // studyinstanceUid - identifier => ValidationUtility.ValidateAccessionNumber(null, FhirTransactionContextBuilder.DefaultAccessionNumber, identifier), // accession number - identifier => ValidationUtility.ValidateAccessionNumber(null, NewAccessionNumber, identifier)); // new accession number - } - - [Fact] - public async Task GivenATransactionContextAndImagingStudyWithExitsingAccessionNumber_WhenProcessedForStudy_ThenAccessionNumberIsNotAddedAsync() - { - ImagingStudy imagingStudy = FhirResourceBuilder.CreateNewImagingStudy(DefaultStudyInstanceUid, new List() { DefaultSeriesInstanceUid }, new List() { DefaultSopInstanceUid }, DefaultPatientResourceId); - FhirTransactionContext context = FhirTransactionContextBuilder.DefaultFhirTransactionContext(FhirTransactionContextBuilder.CreateDicomDataset()); - - await _imagingStudyPropertySynchronizer.SynchronizeAsync(context, imagingStudy); - - Assert.Collection( - imagingStudy.Identifier, - identifier => ValidationUtility.ValidateIdentifier("urn:dicom:uid", $"urn:oid:{DefaultStudyInstanceUid}", identifier), - identifier => ValidationUtility.ValidateAccessionNumber(null, FhirTransactionContextBuilder.DefaultAccessionNumber, identifier)); - - await _imagingStudyPropertySynchronizer.SynchronizeAsync(context, imagingStudy); - - Assert.Collection( - imagingStudy.Identifier, - identifier => ValidationUtility.ValidateIdentifier("urn:dicom:uid", $"urn:oid:{DefaultStudyInstanceUid}", identifier), - identifier => ValidationUtility.ValidateAccessionNumber(null, FhirTransactionContextBuilder.DefaultAccessionNumber, identifier)); - } - - [Fact] - public async Task GivenATransactionContextAndImagingStudyWithNoEndpoint_WhenProcessedForStudy_ThenNewEndpointIsAddedAsync() - { - ImagingStudy imagingStudy = FhirResourceBuilder.CreateNewImagingStudy(DefaultStudyInstanceUid, new List() { DefaultSeriesInstanceUid }, new List() { DefaultSopInstanceUid }, DefaultPatientResourceId); - FhirTransactionContext context = FhirTransactionContextBuilder.DefaultFhirTransactionContext(FhirTransactionContextBuilder.CreateDicomDataset()); - - await _imagingStudyPropertySynchronizer.SynchronizeAsync(context, imagingStudy); - - Assert.Collection( - imagingStudy.Endpoint, - endPoint => Assert.Equal(context.Request.Endpoint.ResourceId.ToResourceReference(), endPoint)); - } - - [Fact] - public async Task GivenATransactionContextAndImagingStudyWithExistingEndpointReference_WhenProcessedForStudy_ThenEndpointResourceIsNotAddedAsync() - { - ImagingStudy imagingStudy = FhirResourceBuilder.CreateNewImagingStudy(DefaultStudyInstanceUid, new List() { DefaultSeriesInstanceUid }, new List() { DefaultSopInstanceUid }, DefaultPatientResourceId); - Endpoint endpoint = FhirResourceBuilder.CreateEndpointResource(); - var endpointResourceId = new ServerResourceId(ResourceType.Endpoint, endpoint.Id); - var endpointReference = endpointResourceId.ToResourceReference(); - - FhirTransactionContext context = FhirTransactionContextBuilder.DefaultFhirTransactionContext(FhirTransactionContextBuilder.CreateDicomDataset()); - context.Request.Endpoint = FhirTransactionRequestEntryGenerator.GenerateDefaultNoChangeRequestEntry(endpointResourceId); - - imagingStudy.Endpoint.Add(endpointReference); - - await _imagingStudyPropertySynchronizer.SynchronizeAsync(context, imagingStudy); - - Assert.Collection( - imagingStudy.Endpoint, - endPoint => Assert.Equal(endpointReference, endPoint)); - } - - [Fact] - public async Task GivenATransactionContextAndImagingStudyWithNewEndpointReference_WhenProcessedForStudyWithEndpoint_ThenEndpointIsAddedAsync() - { - ImagingStudy imagingStudy = FhirResourceBuilder.CreateNewImagingStudy(DefaultStudyInstanceUid, new List() { DefaultSeriesInstanceUid }, new List() { DefaultSopInstanceUid }, DefaultPatientResourceId); - - // Simulate the imaging study with an existing endpoint. - Endpoint existingEndpoint = FhirResourceBuilder.CreateEndpointResource(id: "2345", name: "new wado-rs"); - var existingEndpointResourceId = new ServerResourceId(ResourceType.Endpoint, existingEndpoint.Id); - var existingEndpointReference = existingEndpointResourceId.ToResourceReference(); - - imagingStudy.Endpoint.Add(existingEndpointReference); - - Endpoint endpoint = FhirResourceBuilder.CreateEndpointResource(); - var endpointResourceId = new ServerResourceId(ResourceType.Endpoint, endpoint.Id); - var endpointReference = endpointResourceId.ToResourceReference(); - - FhirTransactionContext context = FhirTransactionContextBuilder.DefaultFhirTransactionContext(FhirTransactionContextBuilder.CreateDicomDataset()); - context.Request.Endpoint = FhirTransactionRequestEntryGenerator.GenerateDefaultNoChangeRequestEntry(endpointResourceId); - - await _imagingStudyPropertySynchronizer.SynchronizeAsync(context, imagingStudy); - - Assert.Collection( - imagingStudy.Endpoint, - endPoint => Assert.Equal(existingEndpointReference, endPoint), - endPoint => Assert.Equal(endpointReference, endPoint)); - } - - [Fact] - public async Task GivenATransactionContexAndImagingStudyWithNewStudyDescription_WhenProcessedForStudy_ThenNewNoteIsAddedAsync() - { - ImagingStudy imagingStudy = FhirResourceBuilder.CreateNewImagingStudy(DefaultStudyInstanceUid, new List() { DefaultSeriesInstanceUid }, new List() { DefaultSopInstanceUid }, DefaultPatientResourceId); - FhirTransactionContext context = FhirTransactionContextBuilder.DefaultFhirTransactionContext(FhirTransactionContextBuilder.CreateDicomDataset()); - - await _imagingStudyPropertySynchronizer.SynchronizeAsync(context, imagingStudy); - - Assert.Collection( - imagingStudy.Note, - note => string.Equals(note.Text.ToString(), "Study Description", StringComparison.Ordinal)); - - // When studyDescription is same, note is not added twice - - await _imagingStudyPropertySynchronizer.SynchronizeAsync(context, imagingStudy); - - Assert.Collection( - imagingStudy.Note, - note => string.Equals(note.Text.ToString(), "Study Description", StringComparison.Ordinal)); - - // When study description is new, new note is added - FhirTransactionContext newConText = FhirTransactionContextBuilder.DefaultFhirTransactionContext(FhirTransactionContextBuilder.CreateDicomDataset(studyDescription: "New Study Description")); - - await _imagingStudyPropertySynchronizer.SynchronizeAsync(newConText, imagingStudy); - - Assert.Collection( - imagingStudy.Note, - note => string.Equals(note.Text.ToString(), "Study Description", StringComparison.Ordinal), - note => string.Equals(note.Text.ToString(), "New Study Description", StringComparison.Ordinal)); - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/ImagingStudy/ImagingStudySeriesPropertySynchronizerTests.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/ImagingStudy/ImagingStudySeriesPropertySynchronizerTests.cs deleted file mode 100644 index 5d573d783d..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/ImagingStudy/ImagingStudySeriesPropertySynchronizerTests.cs +++ /dev/null @@ -1,120 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using Hl7.Fhir.Model; -using Microsoft.Extensions.Options; -using Microsoft.Health.DicomCast.Core.Configurations; -using Microsoft.Health.DicomCast.Core.Features.ExceptionStorage; -using Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; -using NSubstitute; -using Xunit; -using Task = System.Threading.Tasks.Task; - -namespace Microsoft.Health.DicomCast.Core.UnitTests.Features.Worker.FhirTransaction; - -public class ImagingStudySeriesPropertySynchronizerTests -{ - private static readonly CancellationToken DefaultCancellationToken = new CancellationTokenSource().Token; - private readonly IImagingStudySeriesPropertySynchronizer _imagingStudySeriesPropertySynchronizer; - private const string StudyInstanceUid = "111"; - private const string SeriesInstanceUid = "222"; - private const string SopInstanceUid = "333"; - private const string PatientResourceId = "555"; - - private readonly DicomCastConfiguration _dicomCastConfig = new DicomCastConfiguration(); - private readonly IExceptionStore _exceptionStore = Substitute.For(); - - public ImagingStudySeriesPropertySynchronizerTests() - { - _imagingStudySeriesPropertySynchronizer = new ImagingStudySeriesPropertySynchronizer(Options.Create(_dicomCastConfig), _exceptionStore); - } - - [Fact] - public async Task GivenATransactionContexAndImagingStudy_WhenprocessedForSeries_ThenDicomPropertiesAreCorrectlyMappedtoSeriesWithinImagingStudyAsync() - { - ImagingStudy imagingStudy = FhirResourceBuilder.CreateNewImagingStudy(StudyInstanceUid, new List() { SeriesInstanceUid }, new List() { SopInstanceUid }, PatientResourceId); - FhirTransactionContext context = FhirTransactionContextBuilder.DefaultFhirTransactionContext(FhirTransactionContextBuilder.CreateDicomDataset()); - ImagingStudy.SeriesComponent series = imagingStudy.Series.First(); - - await _imagingStudySeriesPropertySynchronizer.SynchronizeAsync(context, series, DefaultCancellationToken); - - Assert.Equal("Series Description", series.Description); - Assert.Equal("MODALITY", series.Modality.Code); - Assert.Equal(1, series.Number); - Assert.Equal(new FhirDateTime(1974, 8, 10, 8, 10, 24, TimeSpan.Zero), series.StartedElement); - } - - [Fact] - public async Task GivenATransactionContextWithUpdatedSeriesDescription_WhenprocessedForSeries_ThenDicomPropertyValuesAreUpdatedAreCorrectlyAsync() - { - ImagingStudy imagingStudy = FhirResourceBuilder.CreateNewImagingStudy(StudyInstanceUid, new List() { SeriesInstanceUid }, new List() { SopInstanceUid }, PatientResourceId); - FhirTransactionContext context = FhirTransactionContextBuilder.DefaultFhirTransactionContext(FhirTransactionContextBuilder.CreateDicomDataset()); - ImagingStudy.SeriesComponent series = imagingStudy.Series.First(); - - await _imagingStudySeriesPropertySynchronizer.SynchronizeAsync(context, series, DefaultCancellationToken); - - Assert.Equal("Series Description", series.Description); - - FhirTransactionContext newContext = FhirTransactionContextBuilder.DefaultFhirTransactionContext(FhirTransactionContextBuilder.CreateDicomDataset(seriesDescrition: "New Series Description")); - - await _imagingStudySeriesPropertySynchronizer.SynchronizeAsync(newContext, series, DefaultCancellationToken); - Assert.Equal("New Series Description", series.Description); - } - - [Fact] - public async Task GivenATransactionContextWithUpdatedSeriesModality_WhenprocessedForSeries_ThenDicomPropertyValuesAreUpdatedAreCorrectlyAsync() - { - ImagingStudy imagingStudy = FhirResourceBuilder.CreateNewImagingStudy(StudyInstanceUid, new List() { SeriesInstanceUid }, new List() { SopInstanceUid }, PatientResourceId); - FhirTransactionContext context = FhirTransactionContextBuilder.DefaultFhirTransactionContext(FhirTransactionContextBuilder.CreateDicomDataset()); - ImagingStudy.SeriesComponent series = imagingStudy.Series.First(); - - await _imagingStudySeriesPropertySynchronizer.SynchronizeAsync(context, series, DefaultCancellationToken); - - Assert.Equal("MODALITY", series.Modality.Code); - - FhirTransactionContext newContext = FhirTransactionContextBuilder.DefaultFhirTransactionContext(FhirTransactionContextBuilder.CreateDicomDataset(modalityInSeries: "NEWMODALITY")); - - await _imagingStudySeriesPropertySynchronizer.SynchronizeAsync(newContext, series, DefaultCancellationToken); - Assert.Equal("NEWMODALITY", series.Modality.Code); - } - - [Fact] - public async Task GivenATransactionContextWithUpdatedSeriesNumber_WhenprocessedForSeries_ThenDicomPropertyValuesAreUpdatedAreCorrectlyAsync() - { - ImagingStudy imagingStudy = FhirResourceBuilder.CreateNewImagingStudy(StudyInstanceUid, new List() { SeriesInstanceUid }, new List() { SopInstanceUid }, PatientResourceId); - FhirTransactionContext context = FhirTransactionContextBuilder.DefaultFhirTransactionContext(FhirTransactionContextBuilder.CreateDicomDataset()); - ImagingStudy.SeriesComponent series = imagingStudy.Series.First(); - - await _imagingStudySeriesPropertySynchronizer.SynchronizeAsync(context, series, DefaultCancellationToken); - - Assert.Equal(1, series.Number); - - FhirTransactionContext newContext = FhirTransactionContextBuilder.DefaultFhirTransactionContext(FhirTransactionContextBuilder.CreateDicomDataset(seriesNumber: "2")); - - await _imagingStudySeriesPropertySynchronizer.SynchronizeAsync(newContext, series, DefaultCancellationToken); - Assert.Equal(2, series.Number); - } - - [Fact] - public async Task GivenATransactionContextWithNoPropertyValueChange_WhenprocessedForSeries_ThenDicomPropertyValuesUpdateIsSkippedAsync() - { - ImagingStudy imagingStudy = FhirResourceBuilder.CreateNewImagingStudy(StudyInstanceUid, new List() { SeriesInstanceUid }, new List() { SopInstanceUid }, PatientResourceId); - FhirTransactionContext context = FhirTransactionContextBuilder.DefaultFhirTransactionContext(); - ImagingStudy.SeriesComponent series = imagingStudy.Series.First(); - - await _imagingStudySeriesPropertySynchronizer.SynchronizeAsync(context, series, DefaultCancellationToken); - - Assert.Equal(1, series.Number); - - FhirTransactionContext newContext = FhirTransactionContextBuilder.DefaultFhirTransactionContext(); - - await _imagingStudySeriesPropertySynchronizer.SynchronizeAsync(newContext, series, DefaultCancellationToken); - Assert.Equal(1, series.Number); - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/ImagingStudy/ImagingStudyUpsertHandlerTests.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/ImagingStudy/ImagingStudyUpsertHandlerTests.cs deleted file mode 100644 index 0d4543a5ee..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/ImagingStudy/ImagingStudyUpsertHandlerTests.cs +++ /dev/null @@ -1,276 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Hl7.Fhir.Model; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Client.Models; -using Microsoft.Health.DicomCast.Core.Configurations; -using Microsoft.Health.DicomCast.Core.Features.ExceptionStorage; -using Microsoft.Health.DicomCast.Core.Features.Fhir; -using Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; -using NSubstitute; -using Xunit; -using Task = System.Threading.Tasks.Task; - -namespace Microsoft.Health.DicomCast.Core.UnitTests.Features.Worker.FhirTransaction; - -public class ImagingStudyUpsertHandlerTests -{ - private const string DefaultDicomWebEndpoint = "https://dicom/"; - - private readonly IFhirService _fhirService; - private readonly IImagingStudySynchronizer _imagingStudySynchronizer; - private readonly ImagingStudyUpsertHandler _imagingStudyUpsertHandler; - private readonly DicomWebConfiguration _configuration; - private readonly DicomCastConfiguration _dicomCastConfig = new DicomCastConfiguration(); - private readonly IExceptionStore _exceptionStore = Substitute.For(); - - private FhirTransactionContext _fhirTransactionContext; - - public ImagingStudyUpsertHandlerTests() - { - _configuration = new DicomWebConfiguration() { Endpoint = new System.Uri(DefaultDicomWebEndpoint), }; - IOptions optionsConfiguration = Options.Create(_configuration); - - _fhirService = Substitute.For(); - _imagingStudySynchronizer = new ImagingStudySynchronizer(new ImagingStudyPropertySynchronizer(Options.Create(_dicomCastConfig), _exceptionStore), new ImagingStudySeriesPropertySynchronizer(Options.Create(_dicomCastConfig), _exceptionStore), new ImagingStudyInstancePropertySynchronizer(Options.Create(_dicomCastConfig), _exceptionStore)); - _imagingStudyUpsertHandler = new ImagingStudyUpsertHandler(_fhirService, _imagingStudySynchronizer, optionsConfiguration); - } - - [Fact] - public async Task GivenAValidCreateChangeFeed_WhenBuilt_ThenCorrectEntryComponentShouldBeCreated() - { - const string studyInstanceUid = "1"; - const string seriesInstanceUid = "2"; - const string sopInstanceUid = "3"; - const string patientResourceId = "p1"; - - ChangeFeedEntry changeFeedEntry = ChangeFeedGenerator.Generate( - action: ChangeFeedAction.Create, - studyInstanceUid: studyInstanceUid, - seriesInstanceUid: seriesInstanceUid, - sopInstanceUid: sopInstanceUid); - - FhirTransactionRequestEntry entry = await BuildImagingStudyEntryComponent(studyInstanceUid, seriesInstanceUid, sopInstanceUid, patientResourceId); - - ValidationUtility.ValidateRequestEntryMinimumRequirementForWithChange(FhirTransactionRequestMode.Create, "ImagingStudy", Bundle.HTTPVerb.POST, entry); - - ImagingStudy imagingStudy = Assert.IsType(entry.Resource); - - string jsonString; - jsonString = JsonSerializer.Serialize(entry); - - Assert.IsType(entry.ResourceId); - Assert.Equal(ImagingStudy.ImagingStudyStatus.Available, imagingStudy.Status); - Assert.Null(entry.Request.IfMatch); - - ValidationUtility.ValidateResourceReference("Patient/p1", imagingStudy.Subject); - - Assert.Collection( - imagingStudy.Identifier, - identifier => ValidationUtility.ValidateIdentifier("urn:dicom:uid", $"urn:oid:{studyInstanceUid}", identifier), - identifier => ValidationUtility.ValidateAccessionNumber(null, FhirTransactionContextBuilder.DefaultAccessionNumber, identifier)); - - Assert.Collection( - imagingStudy.Series, - series => - { - Assert.Equal(seriesInstanceUid, series.Uid); - - Assert.Collection( - series.Instance, - instance => Assert.Equal(sopInstanceUid, instance.Uid)); - }); - - ValidateDicomFilePropertiesAreCorrectlyMapped(imagingStudy, series: imagingStudy.Series.First(), instance: imagingStudy.Series.First().Instance.First()); - } - - [Fact] - public async Task GivenAChangeFeedWithNewSeriesAndInstanceForAnExistingImagingStudy_WhenBuilt_ThenCorrectEntryComponentShouldBeCreated() - { - const string studyInstanceUid = "1"; - const string seriesInstanceUid = "2"; - const string sopInstanceUid = "3"; - const string patientResourceId = "p1"; - const string newSeriesInstanceUid = "3"; - const string newSopInstanceUid = "4"; - - ImagingStudy imagingStudy = FhirResourceBuilder.CreateNewImagingStudy(studyInstanceUid, new List() { seriesInstanceUid }, new List() { sopInstanceUid }, patientResourceId); - - _fhirService.RetrieveImagingStudyAsync(Arg.Any(), Arg.Any()).Returns(imagingStudy); - - // Update an existing ImagingStudy - FhirTransactionRequestEntry entry = await BuildImagingStudyEntryComponent(studyInstanceUid, newSeriesInstanceUid, newSopInstanceUid, patientResourceId); - - ImagingStudy updatedImagingStudy = ValidationUtility.ValidateImagingStudyUpdate(studyInstanceUid, patientResourceId, entry); - - Assert.Collection( - updatedImagingStudy.Series, - series => - { - ValidationUtility.ValidateSeries(series, seriesInstanceUid, sopInstanceUid); - }, - series => - { - ValidationUtility.ValidateSeries(series, newSeriesInstanceUid, newSopInstanceUid); - }); - } - - [Fact] - public async Task GivenAChangeFeedWithNewInstanceForAnExistingSeriesAndImagingStudy_WhenBuilt_ThenCorrectEntryComponentShouldBeCreated() - { - const string studyInstanceUid = "1"; - const string seriesInstanceUid = "2"; - const string sopInstanceUid = "3"; - const string patientResourceId = "p1"; - const string newSopInstanceUid = "4"; - - // create a new ImagingStudy - ImagingStudy imagingStudy = FhirResourceBuilder.CreateNewImagingStudy(studyInstanceUid, new List() { seriesInstanceUid }, new List() { sopInstanceUid }, patientResourceId); - - _fhirService.RetrieveImagingStudyAsync(Arg.Any(), Arg.Any()).Returns(imagingStudy); - - // update an existing ImagingStudy - FhirTransactionRequestEntry entry = await BuildImagingStudyEntryComponent(studyInstanceUid, seriesInstanceUid, newSopInstanceUid, patientResourceId); - - ImagingStudy updatedImagingStudy = ValidationUtility.ValidateImagingStudyUpdate(studyInstanceUid, patientResourceId, entry); - - Assert.Collection( - updatedImagingStudy.Series, - series => - { - ValidationUtility.ValidateSeries(series, seriesInstanceUid, sopInstanceUid, newSopInstanceUid); - }); - } - - [Fact] - public async Task GivenAChangeFeedWithExistingInstanceForAnExistingSeriesAndImagingStudy_WhenBuilt_ThenNoEntryComponentShouldBeReturned() - { - const string studyInstanceUid = "1"; - const string seriesInstanceUid = "2"; - const string sopInstanceUid = "3"; - const string patientResourceId = "p1"; - - // create a new ImagingStudy - ImagingStudy imagingStudy = FhirResourceBuilder.CreateNewImagingStudy(studyInstanceUid, new List() { seriesInstanceUid }, new List() { sopInstanceUid }, $"Patient/{patientResourceId}"); - - _fhirService.RetrieveImagingStudyAsync(Arg.Any(), Arg.Any()).Returns(imagingStudy); - - // try update an existing ImagingStudy - FhirTransactionRequestEntry entry = await BuildImagingStudyEntryComponent(studyInstanceUid, seriesInstanceUid, sopInstanceUid, patientResourceId, addMetadata: false); - - // Validate no entry component is created as there is no update - Assert.NotNull(entry); - Assert.Equal(FhirTransactionRequestMode.None, entry.RequestMode); - Assert.Null(entry.Request); - Assert.IsType(entry.ResourceId); - Assert.True(imagingStudy.IsExactly(entry.Resource)); - } - - [Fact] - public async Task GivenAChangeFeedWithNewInstanceAndNewSeiresForAnExistingImagingStudy_WhenBuilt_ThenCorrectEtagIsGenerated() - { - const string studyInstanceUid = "1"; - const string seriesInstanceUid = "2"; - const string sopInstanceUid = "3"; - const string newSeriesInstanceUid = "3"; - const string newSopInstanceUid = "3"; - const string patientResourceId = "p1"; - - // create a new ImagingStudy - ImagingStudy imagingStudy = FhirResourceBuilder.CreateNewImagingStudy(studyInstanceUid, new List() { seriesInstanceUid }, new List() { sopInstanceUid }, patientResourceId); - - _fhirService.RetrieveImagingStudyAsync(Arg.Any(), Arg.Any()).Returns(imagingStudy); - - // update an existing ImagingStudy - FhirTransactionRequestEntry entry = await BuildImagingStudyEntryComponent(studyInstanceUid, newSeriesInstanceUid, newSopInstanceUid, patientResourceId); - - string expectedIfMatchCondition = $"W/\"1\""; - - Assert.Equal(expectedIfMatchCondition, entry.Request.IfMatch); - } - - [Fact] - public async Task GivenAChangeFeedWithNewInstanceAndNewSeiresForAnExistingImagingStudy_WhenBuilt_ThenCorrectEtagIsGeneratedd() - { - const string studyInstanceUid = "1"; - const string seriesInstanceUid = "2"; - const string sopInstanceUid = "3"; - const string newSeriesInstanceUid = "3"; - const string newSopInstanceUid = "3"; - const string patientResourceId = "p1"; - - // create a new ImagingStudy - ImagingStudy imagingStudy = FhirResourceBuilder.CreateNewImagingStudy(studyInstanceUid, new List() { seriesInstanceUid }, new List() { sopInstanceUid }, patientResourceId); - - _fhirService.RetrieveImagingStudyAsync(Arg.Any(), Arg.Any()).Returns(imagingStudy); - - // update an existing ImagingStudy - FhirTransactionRequestEntry entry = await BuildImagingStudyEntryComponent(studyInstanceUid, newSeriesInstanceUid, newSopInstanceUid, patientResourceId); - - string expectedIfMatchCondition = $"W/\"1\""; - - Assert.Equal(expectedIfMatchCondition, entry.Request.IfMatch); - } - - private async Task BuildImagingStudyEntryComponent(string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid, string patientResourceId, bool addMetadata = true) - { - ChangeFeedEntry changeFeedEntry = ChangeFeedGenerator.Generate( - action: ChangeFeedAction.Create, - studyInstanceUid: studyInstanceUid, - seriesInstanceUid: seriesInstanceUid, - sopInstanceUid: sopInstanceUid, - metadata: addMetadata ? FhirTransactionContextBuilder.CreateDicomDataset() : null); - - return await PrepareRequestAsync(changeFeedEntry, patientResourceId); - } - - private async Task PrepareRequestAsync(ChangeFeedEntry changeFeedEntry, string patientResourceId) - { - _fhirTransactionContext = new FhirTransactionContext(changeFeedEntry); - - _fhirTransactionContext.Request.Patient = FhirTransactionRequestEntryGenerator.GenerateDefaultNoChangeRequestEntry( - new ServerResourceId(ResourceType.Patient, patientResourceId)); - - _fhirTransactionContext.Request.Endpoint = FhirTransactionRequestEntryGenerator.GenerateDefaultNoChangeRequestEntry( - new ServerResourceId(ResourceType.Endpoint, "endpoint")); - - return await _imagingStudyUpsertHandler.BuildAsync(_fhirTransactionContext, CancellationToken.None); - } - - private void ValidateDicomFilePropertiesAreCorrectlyMapped(ImagingStudy imagingStudy, ImagingStudy.SeriesComponent series, ImagingStudy.InstanceComponent instance) - { - Assert.Collection( - imagingStudy.Endpoint, - reference => string.Equals(reference.Reference, _fhirTransactionContext.Request.Endpoint.ToString(), StringComparison.Ordinal)); - - // Assert imaging study properties are mapped correctly - Assert.Collection( - imagingStudy.Modality, - modality => string.Equals(modality.Code, "MODALITY", StringComparison.Ordinal)); - - Assert.Collection( - imagingStudy.Note, - note => string.Equals(note.Text.ToString(), "Study Description", StringComparison.Ordinal)); - - Assert.Equal(new FhirDateTime(1974, 7, 10, 7, 10, 24, TimeSpan.Zero), imagingStudy.StartedElement); - - // Assert series properties are mapped correctly - Assert.Equal("Series Description", series.Description); - Assert.Equal("MODALITY", series.Modality.Code); - Assert.Equal(1, series.Number); - Assert.Equal(new FhirDateTime(1974, 8, 10, 8, 10, 24, TimeSpan.Zero), series.StartedElement); - - // Assert instance properties are mapped correctly - Assert.Equal("4444", instance.SopClass.Code); - Assert.Equal(1, instance.Number); - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/MockFhirTransactionPipelineStep.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/MockFhirTransactionPipelineStep.cs deleted file mode 100644 index ebb128dd06..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/MockFhirTransactionPipelineStep.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -namespace Microsoft.Health.DicomCast.Core.UnitTests.Features.Worker.FhirTransaction; - -public class MockFhirTransactionPipelineStep : IFhirTransactionPipelineStep -{ - public Action OnPrepareRequestAsyncCalled { get; set; } - - public Action OnProcessResponseCalled { get; set; } - - public Task PrepareRequestAsync(FhirTransactionContext context, CancellationToken cancellationToken) - { - OnPrepareRequestAsyncCalled?.Invoke(context, cancellationToken); - - return Task.CompletedTask; - } - - public void ProcessResponse(FhirTransactionContext context) - { - OnProcessResponseCalled?.Invoke(context); - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/Observation/ObservationParserTests.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/Observation/ObservationParserTests.cs deleted file mode 100644 index 1a5e534c03..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/Observation/ObservationParserTests.cs +++ /dev/null @@ -1,278 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Linq; -using FellowOakDicom; -using FellowOakDicom.StructuredReport; -using Hl7.Fhir.Model; -using Microsoft.Health.DicomCast.Core.Features.Fhir; -using Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; -using Xunit; - -namespace Microsoft.Health.DicomCast.Core.UnitTests.Features.Worker.FhirTransaction; - -public class ObservationParserTests -{ - [Fact] - public void RadiationEventWithAllSupportedAttributes() - { - const string randomIrradiationEventUid = "1.2.3.4.5.6.123123"; - const decimal randomDecimalNumber = (decimal)0.10; - var randomRadiationMeasurementCodeItem = new DicomCodeItem("mGy", "UCUM", "mGy"); - var report = new DicomStructuredReport( - ObservationConstants.IrradiationEventXRayData, - new DicomContentItem( - ObservationConstants.IrradiationEventUid, - DicomRelationship.Contains, - new DicomUID(randomIrradiationEventUid, "", DicomUidType.Unknown)), - new DicomContentItem( - ObservationConstants.MeanCtdIvol, - DicomRelationship.Contains, - new DicomMeasuredValue(randomDecimalNumber, - randomRadiationMeasurementCodeItem)), - new DicomContentItem( - ObservationConstants.Dlp, - DicomRelationship.Contains, - new DicomMeasuredValue(randomDecimalNumber, - randomRadiationMeasurementCodeItem)), - new DicomContentItem( - ObservationConstants.CtdIwPhantomType, - DicomRelationship.Contains, - new DicomCodeItem("113691", "DCM", "IEC Body Dosimetry Phantom"))); - - var observations = ObservationParser.Parse( - report.Dataset, - new ResourceReference(), - new ResourceReference(), - IdentifierUtility.CreateIdentifier(randomIrradiationEventUid)); - Assert.Single(observations); - - Observation radiationEvent = observations.First(); - Assert.Single(radiationEvent.Identifier); - Assert.Equal("urn:oid:" + randomIrradiationEventUid, radiationEvent.Identifier[0].Value); - Assert.Equal(2, - radiationEvent.Component - .Count(component => component.Value is Quantity)); - Assert.Equal(1, - radiationEvent.Component - .Count(component => component.Value is CodeableConcept)); - } - - [Fact] - public void DoseSummaryWithAllSupportedAttributes() - { - const string studyInstanceUid = "1.3.12.2.123.5.4.5.123123.123123"; - const string accessionNumber = "random-accession"; - const decimal randomDecimalNumber = (decimal)0.10; - var randomRadiationMeasurementCodeItem = new DicomCodeItem("mGy", "UCUM", "mGy"); - - var report = new DicomStructuredReport( - ObservationConstants.RadiopharmaceuticalRadiationDoseReport, - // identifiers - new DicomContentItem( - ObservationConstants.StudyInstanceUid, - DicomRelationship.HasProperties, - new DicomUID(studyInstanceUid, "", DicomUidType.Unknown)), - new DicomContentItem( - ObservationConstants.AccessionNumber, - DicomRelationship.HasProperties, - DicomValueType.Text, - accessionNumber), - - // attributes - new DicomContentItem( - ObservationConstants.DoseRpTotal, - DicomRelationship.Contains, - new DicomMeasuredValue(randomDecimalNumber, - randomRadiationMeasurementCodeItem)), - new DicomContentItem( - ObservationConstants.AccumulatedAverageGlandularDose, - DicomRelationship.Contains, - new DicomMeasuredValue(randomDecimalNumber, - randomRadiationMeasurementCodeItem)), - new DicomContentItem( - ObservationConstants.DoseAreaProductTotal, - DicomRelationship.Contains, - new DicomMeasuredValue(randomDecimalNumber, - randomRadiationMeasurementCodeItem)), - new DicomContentItem( - ObservationConstants.FluoroDoseAreaProductTotal, - DicomRelationship.Contains, - new DicomMeasuredValue(randomDecimalNumber, - randomRadiationMeasurementCodeItem)), - new DicomContentItem( - ObservationConstants.AcquisitionDoseAreaProductTotal, - DicomRelationship.Contains, - new DicomMeasuredValue(randomDecimalNumber, - randomRadiationMeasurementCodeItem)), - new DicomContentItem( - ObservationConstants.TotalFluoroTime, - DicomRelationship.Contains, - new DicomMeasuredValue(randomDecimalNumber, - randomRadiationMeasurementCodeItem)), - new DicomContentItem( - ObservationConstants.TotalNumberOfRadiographicFrames, - DicomRelationship.Contains, - new DicomMeasuredValue(10, - new DicomCodeItem("1", "UCUM", "No units"))), - new DicomContentItem( - ObservationConstants.AdministeredActivity, - DicomRelationship.Contains, - new DicomMeasuredValue(randomDecimalNumber, - randomRadiationMeasurementCodeItem)), - new DicomContentItem( - ObservationConstants.CtDoseLengthProductTotal, - DicomRelationship.Contains, - new DicomMeasuredValue(randomDecimalNumber, - randomRadiationMeasurementCodeItem)), - new DicomContentItem( - ObservationConstants.TotalNumberOfIrradiationEvents, - DicomRelationship.Contains, - new DicomMeasuredValue(10, - new DicomCodeItem("1", "UCUM", "No units"))), - new DicomContentItem( - ObservationConstants.MeanCtdIvol, - DicomRelationship.Contains, - new DicomMeasuredValue(randomDecimalNumber, - randomRadiationMeasurementCodeItem)), - new DicomContentItem( - ObservationConstants.RadiopharmaceuticalAgent, - DicomRelationship.Contains, - DicomValueType.Text, - "Uranium"), - new DicomContentItem( - ObservationConstants.Radionuclide, - DicomRelationship.Contains, - DicomValueType.Text, - "Uranium"), - new DicomContentItem( - ObservationConstants.RadiopharmaceuticalVolume, - DicomRelationship.Contains, - new DicomMeasuredValue(randomDecimalNumber, - randomRadiationMeasurementCodeItem)), - new DicomContentItem( - ObservationConstants.RouteOfAdministration, - DicomRelationship.Contains, - new DicomCodeItem("needle", "random-scheme", "this is made up")) - ); - - var observations = ObservationParser.Parse( - report.Dataset, - new ResourceReference(), - new ResourceReference(), - IdentifierUtility.CreateIdentifier(studyInstanceUid)); - - Assert.Single(observations); - - Observation doseSummary = observations.First(); - Assert.Equal(2, doseSummary.Identifier.Count); - Assert.Equal("urn:oid:" + studyInstanceUid, - doseSummary.Identifier[0].Value); - Assert.Equal(accessionNumber, - doseSummary.Identifier[1].Value); - Assert.Equal(10, - doseSummary.Component - .Count(component => component.Value is Quantity)); - Assert.Equal(2, - doseSummary.Component - .Count(component => component.Value is Integer)); - Assert.Equal(2, - doseSummary.Component - .Count(component => component.Value is FhirString)); - Assert.Equal(1, - doseSummary.Component - .Count(component => component.Value is CodeableConcept)); - } - - [Fact] - public void DoseSummaryWithStudyInstanceUidInTag() - { - var report = new DicomStructuredReport( - ObservationConstants.XRayRadiationDoseReport); - report.Dataset - .Add(DicomTag.StudyInstanceUID, "12345") - .Add(DicomTag.AccessionNumber, "12345"); - - var observations = ObservationParser.Parse( - report.Dataset, - new ResourceReference(), - new ResourceReference(), - new Identifier()); - Assert.Single(observations); - } - - - [Fact] - public void DoseSummaryWithStudyInstanceUidInReport() - { - var report = new DicomStructuredReport( - ObservationConstants.XRayRadiationDoseReport, - new DicomContentItem( - ObservationConstants.StudyInstanceUid, - DicomRelationship.HasProperties, - new DicomUID("1.3.12.2.123.5.4.5.123123.123123", "", DicomUidType.Unknown))); - - var observations = ObservationParser.Parse( - report.Dataset, - new ResourceReference(), - new ResourceReference(), - new Identifier()); - Assert.Single(observations); - } - - [Fact] - public void RadiationEventWithIrradiationEventUid() - { - var report = new DicomStructuredReport( - ObservationConstants.IrradiationEventXRayData, - new DicomContentItem( - ObservationConstants.IrradiationEventUid, - DicomRelationship.Contains, - new DicomUID("1.3.12.2.1234.5.4.5.123123.3000000111", "foobar", DicomUidType.Unknown) - )); - - var observations = ObservationParser.Parse( - report.Dataset, - new ResourceReference(), - new ResourceReference(), - new Identifier()); - Assert.Single(observations); - } - - [Fact] - public void RadiationEventWithoutIrradiationEventUid() - { - var report = new DicomStructuredReport( - ObservationConstants.IrradiationEventXRayData); - - var observations = ObservationParser.Parse( - report.Dataset, - new ResourceReference(), - new ResourceReference(), - new Identifier()); - Assert.Empty(observations); - } - - [Fact] - public void RadiationEventWithInvalidDataNotInCodeCodeNoErrorThrown() - { - var report = new DicomStructuredReport( - ObservationConstants.IrradiationEventXRayData, - new DicomContentItem( - ObservationConstants.IrradiationEventUid, - DicomRelationship.Contains, - new DicomUID("1.3.12.2.1234.5.4.5.123123.3000000111", "foobar", DicomUidType.Unknown) - )); - report.Dataset.NotValidated(); - report.Dataset.Add(DicomTag.PatientBirthDateInAlternativeCalendar, "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz"); - - var observations = ObservationParser.Parse( - report.Dataset, - new ResourceReference(), - new ResourceReference(), - new Identifier()); - Assert.Single(observations); - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/Patient/PatientBirthDateSynchronizerTests.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/Patient/PatientBirthDateSynchronizerTests.cs deleted file mode 100644 index 9c11eda519..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/Patient/PatientBirthDateSynchronizerTests.cs +++ /dev/null @@ -1,68 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using FellowOakDicom; -using Hl7.Fhir.Model; -using Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; -using Xunit; - -namespace Microsoft.Health.DicomCast.Core.UnitTests.Features.Worker.FhirTransaction; - -public class PatientBirthDateSynchronizerTests -{ - private readonly PatientBirthDateSynchronizer _patientBirthDateSynchronizer = new PatientBirthDateSynchronizer(); - - private readonly Patient _patient = new Patient(); - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void GivenNoPatientBirthDate_WhenSynchronized_ThenNoBirthDateShouldBeAdded(bool isNewPatient) - { - _patientBirthDateSynchronizer.Synchronize(new DicomDataset(), _patient, isNewPatient); - - Assert.Null(_patient.BirthDate); - } - - [Fact] - public void GivenBirthDateWhileCreating_WhenSynchronized_ThenCorrectBirthDateShouldBeAdded() - { - DateTime birthDate = new DateTime(1990, 01, 01, 12, 12, 12); - _patientBirthDateSynchronizer.Synchronize(CreateDicomDataset(birthDate), _patient, true); - - ValidateDate(birthDate, _patient.BirthDateElement); - } - - [Fact] - public void GivenBirthDateWithExistingPatient_WhenSynchronized_ThenNoBirthDateShouldBeAdded() - { - DateTime birthDate = new DateTime(1990, 01, 01, 12, 12, 12); - - _patientBirthDateSynchronizer.Synchronize(CreateDicomDataset(birthDate), _patient, false); - - Assert.Null(_patient.BirthDate); - } - - private static DicomDataset CreateDicomDataset(DateTime patientBirthDate) - => new DicomDataset() - { - { DicomTag.PatientBirthDate, patientBirthDate }, - }; - - private static void ValidateDate( - DateTime? expectedDate, - Date actualDate) - { - if (!expectedDate.HasValue) - { - Assert.Null(actualDate); - return; - } - - Assert.NotNull(actualDate.ToDateTimeOffset()); - Assert.Equal(expectedDate.Value.Date, actualDate.ToDateTimeOffset().Value.DateTime); - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/Patient/PatientGenderSynchronizerTests.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/Patient/PatientGenderSynchronizerTests.cs deleted file mode 100644 index f2c3d3c356..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/Patient/PatientGenderSynchronizerTests.cs +++ /dev/null @@ -1,52 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using FellowOakDicom; -using Hl7.Fhir.Model; -using Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; -using Xunit; - -namespace Microsoft.Health.DicomCast.Core.UnitTests.Features.Worker.FhirTransaction; - -public class PatientGenderSynchronizerTests -{ - private readonly PatientGenderSynchronizer _patientGenderSynchronizer = new PatientGenderSynchronizer(); - - [Theory] - [InlineData("M", AdministrativeGender.Male, true)] - [InlineData("M", AdministrativeGender.Male, false)] - [InlineData("F", AdministrativeGender.Female, true)] - [InlineData("F", AdministrativeGender.Female, false)] - [InlineData("O", AdministrativeGender.Other, true)] - [InlineData("O", AdministrativeGender.Other, false)] - [InlineData("", null, true)] - [InlineData("", null, false)] - public void GivenAValidPatientSexTag_WhenSynchronized_ThenCorrectGenderShouldBeAssigned(string inputGender, AdministrativeGender? expectedGender, bool isNewPatient) - { - var dataset = new DicomDataset() - { - { DicomTag.PatientSex, inputGender }, - }; - - var patient = new Patient(); - - _patientGenderSynchronizer.Synchronize(dataset, patient, isNewPatient); - - Assert.Equal(expectedGender, patient.Gender); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void GivenAnInvalidPatientGender_WhenBuilt_ThenInvalidDicomTagValueExceptionShouldBeThrown(bool isNewPatient) - { - var dataset = new DicomDataset() - { - { DicomTag.PatientSex, "D" }, - }; - - Assert.Throws(() => _patientGenderSynchronizer.Synchronize(dataset, new Patient(), isNewPatient)); - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/Patient/PatientNameSynchronizerTests.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/Patient/PatientNameSynchronizerTests.cs deleted file mode 100644 index ba6a02e1c7..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/Patient/PatientNameSynchronizerTests.cs +++ /dev/null @@ -1,251 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using FellowOakDicom; -using Hl7.Fhir.Model; -using Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; -using Xunit; - -namespace Microsoft.Health.DicomCast.Core.UnitTests.Features.Worker.FhirTransaction; - -public class PatientNameSynchronizerTests -{ - private readonly PatientNameSynchronizer _patientNameSynchronizer = new PatientNameSynchronizer(); - - private readonly Patient _patient = new Patient(); - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void GivenNoPatientName_WhenSynchronized_ThenNoNameShouldBeAdded(bool isNewPatient) - { - _patientNameSynchronizer.Synchronize(new DicomDataset(), _patient, isNewPatient); - - Assert.Empty(_patient.Name); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void GivenFamilyName_WhenSynchronized_ThenCorrectNameShouldBeAdded(bool isNewPatient) - { - const string familyName = "fn"; - - _patientNameSynchronizer.Synchronize(CreateDicomDataset(familyName), _patient, isNewPatient); - - Assert.Collection( - _patient.Name, - name => ValidateName(name, expectedFamilyName: familyName)); - } - - [Theory] - [InlineData(true, "^")] - [InlineData(false, "^")] - [InlineData(true, "^gn", "gn")] - [InlineData(false, "^gn", "gn")] - [InlineData(true, "^gn1 gn2", "gn1", "gn2")] - [InlineData(false, "^gn1 gn2", "gn1", "gn2")] - public void GivenGivenNames_WhenSynchronized_ThenCorrectNameShouldBeAdded(bool isNewPatient, string inputName, params string[] givenNames) - { - _patientNameSynchronizer.Synchronize(CreateDicomDataset(inputName), _patient, isNewPatient); - - Assert.Collection( - _patient.Name, - name => ValidateName(name, expectedGivenNames: givenNames)); - } - - [Theory] - [InlineData(true, "^^")] - [InlineData(false, "^^")] - [InlineData(true, "^^mn", "mn")] - [InlineData(false, "^^mn", "mn")] - [InlineData(true, "^^mn1 mn2", "mn1", "mn2")] - [InlineData(false, "^^mn1 mn2", "mn1", "mn2")] - public void GivenMiddleNames_WhenSynchronized_ThenCorrectNameShouldBeAdded(bool isNewPatient, string inputName, params string[] givenNames) - { - _patientNameSynchronizer.Synchronize(CreateDicomDataset(inputName), _patient, isNewPatient); - - Assert.Collection( - _patient.Name, - name => ValidateName(name, expectedGivenNames: givenNames)); - } - - [Theory] - [InlineData(true, "^^^")] - [InlineData(false, "^^^")] - [InlineData(true, "^^^p1", "p1")] - [InlineData(false, "^^^p1", "p1")] - [InlineData(true, "^^^p1 p2", "p1", "p2")] - [InlineData(false, "^^^p1 p2", "p1", "p2")] - public void GivenPrefixes_WhenSynchronized_ThenCorrectNameShouldBeAdded(bool isNewPatient, string inputName, params string[] prefixes) - { - _patientNameSynchronizer.Synchronize(CreateDicomDataset(inputName), _patient, isNewPatient); - - Assert.Collection( - _patient.Name, - name => ValidateName(name, expectedPrefixes: prefixes)); - } - - [Theory] - [InlineData(true, "^^^^")] - [InlineData(false, "^^^^")] - [InlineData(true, "^^^^s1", "s1")] - [InlineData(false, "^^^^s1", "s1")] - [InlineData(true, "^^^^s1 s2", "s1", "s2")] - [InlineData(false, "^^^^s1 s2", "s1", "s2")] - public void GivenSuffixes_WhenSynchronized_ThenCorrectNameShouldBeAdded(bool isNewPatient, string inputName, params string[] suffixes) - { - _patientNameSynchronizer.Synchronize(CreateDicomDataset(inputName), _patient, isNewPatient); - - Assert.Collection( - _patient.Name, - name => ValidateName(name, expectedSuffixes: suffixes)); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void GivenNameWithSpacePadding_WhenSynchronized_ThenPaddingShouldBeRemoved(bool isNewPatient) - { - _patientNameSynchronizer.Synchronize( - CreateDicomDataset(" Doe^Joe "), - _patient, - isNewPatient); - - Assert.Collection( - _patient.Name, - name => ValidateName( - name, - expectedFamilyName: "Doe", - expectedGivenNames: new[] { "Joe" })); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void GivenName_WhenSynchronized_ThenCorrectNameShouldBeAdded(bool isNewPatient) - { - _patientNameSynchronizer.Synchronize( - CreateDicomDataset("Adams^John Robert^Quincy^Rev.^B.A. M.Div."), - _patient, - isNewPatient); - - Assert.Collection( - _patient.Name, - name => ValidateName( - name, - expectedFamilyName: "Adams", - expectedGivenNames: new[] { "John", "Robert", "Quincy" }, - expectedPrefixes: new[] { "Rev." }, - expectedSuffixes: new[] { "B.A.", "M.Div." })); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void GivenNameAlreadyExists_WhenSynchronized_ThenItWillBeOverwritten(bool isNewPatient) - { - _patient.Name.Add(new HumanName() - { - Use = HumanName.NameUse.Usual, - Family = "Smith", - Given = new[] { "John" }, - }); - - _patientNameSynchronizer.Synchronize( - CreateDicomDataset("Morrison-Jones^Susan^^^Ph.D., Chief Executive Officer"), - _patient, - isNewPatient); - - // The spec says the suffix should be two, but I am not sure how we can do that without - // some sort of natural language interpretation. For now, because we are separating by space, - // the "Chief Executive Officer" will be split into 3 different suffixes. - Assert.Collection( - _patient.Name, - name => ValidateName( - name, - expectedFamilyName: "Morrison-Jones", - expectedGivenNames: new[] { "Susan" }, - expectedSuffixes: new[] { "Ph.D.,", "Chief", "Executive", "Officer" })); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void GivenOtherName_WhenSynchronized_ThenCorrectNameShouldBeAdded(bool isNewPatient) - { - _patient.Name.Add(new HumanName() - { - Use = HumanName.NameUse.Usual, - Family = "Smith", - Given = new[] { "John" }, - }); - - _patient.Name.Add(new HumanName() - { - Use = HumanName.NameUse.Official, - Family = "Smith", - Given = new[] { "John" }, - }); - - _patientNameSynchronizer.Synchronize( - CreateDicomDataset("Schmith^Johnny"), - _patient, - isNewPatient); - - // The spec says the suffix should be two, but I am not sure how we can do that without - // some sort of natural language interpretation. For now, because we are separating by space, - // the "Chief Executive Officer" will be split into 3 different suffixes. - Assert.Collection( - _patient.Name, - name => ValidateName( - name, - expectedFamilyName: "Schmith", - expectedGivenNames: new[] { "Johnny" }), - name => ValidateName( - name, - expectedUse: HumanName.NameUse.Official, - expectedFamilyName: "Smith", - expectedGivenNames: new[] { "John" })); - } - - private static DicomDataset CreateDicomDataset(string patientName) - => new DicomDataset() - { - { DicomTag.PatientName, patientName }, - }; - - private static void ValidateName( - HumanName actualName, - HumanName.NameUse expectedUse = HumanName.NameUse.Usual, - string expectedFamilyName = "", - string[] expectedGivenNames = null, - string[] expectedPrefixes = null, - string[] expectedSuffixes = null) - { - if (expectedGivenNames == null) - { - expectedGivenNames = Array.Empty(); - } - - if (expectedPrefixes == null) - { - expectedPrefixes = Array.Empty(); - } - - if (expectedSuffixes == null) - { - expectedSuffixes = Array.Empty(); - } - - Assert.NotNull(actualName); - Assert.Equal(expectedUse, actualName.Use); - Assert.Equal(expectedFamilyName, actualName.Family); - Assert.Equal(expectedGivenNames, actualName.Given); - Assert.Equal(expectedPrefixes, actualName.Prefix); - Assert.Equal(expectedSuffixes, actualName.Suffix); - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/Patient/PatientPipelineStepTests.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/Patient/PatientPipelineStepTests.cs deleted file mode 100644 index e6a10a7be3..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/Patient/PatientPipelineStepTests.cs +++ /dev/null @@ -1,272 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Net; -using System.Threading; -using FellowOakDicom; -using Hl7.Fhir.Model; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using Microsoft.Health.DicomCast.Core.Configurations; -using Microsoft.Health.DicomCast.Core.Extensions; -using Microsoft.Health.DicomCast.Core.Features.Fhir; -using Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; -using NSubstitute; -using Xunit; -using Task = System.Threading.Tasks.Task; - -namespace Microsoft.Health.DicomCast.Core.UnitTests.Features.Worker.FhirTransaction; - -public class PatientPipelineStepTests -{ - private static readonly CancellationToken DefaultCancellationToken = new CancellationTokenSource().Token; - - private const string DefaultPatientId = "p1"; - private const string DefaultPatientSystemId = "patientSystemId"; - private static readonly DicomDataset DefaultDicomDataset = new DicomDataset() - { - { DicomTag.PatientID, DefaultPatientId }, - }; - - private readonly IFhirService _fhirService = Substitute.For(); - private readonly IPatientSynchronizer _patientSynchronizer = Substitute.For(); - private PatientPipelineStep _patientPipeline; - private PatientConfiguration _patientConfiguration; - - public PatientPipelineStepTests() - { - _patientConfiguration = new PatientConfiguration() { PatientSystemId = "patientSystemId", IsIssuerIdUsed = false }; - IOptions optionsConfiguration = Options.Create(_patientConfiguration); - - _patientPipeline = new PatientPipelineStep(_fhirService, _patientSynchronizer, optionsConfiguration, NullLogger.Instance); - } - - [Fact] - public async Task GivenNullMetadata_WhenRequestIsPrepared_ThenItShouldNotCreateEntryComponent() - { - var context = new FhirTransactionContext(ChangeFeedGenerator.Generate()); - - await _patientPipeline.PrepareRequestAsync(context, DefaultCancellationToken); - - Assert.Null(context.Request.Patient); - } - - [Fact] - public async Task GivenMissingPatientIdTag_WhenPreparingTheRequest_ThenMissingRequiredDicomTagExceptionShouldBeThrown() - { - var context = new FhirTransactionContext(ChangeFeedGenerator.Generate(metadata: new DicomDataset())); - - await Assert.ThrowsAsync(() => _patientPipeline.PrepareRequestAsync(context, DefaultCancellationToken)); - } - - [Fact] - public async Task GivenPatientIdTagPresentWithMissingValue_WhenPreparingTheRequest_ThenMissingRequiredDicomTagExceptionShouldBeThrown() - { - DicomDataset dicomDataset = new DicomDataset(); - dicomDataset.AddOrUpdate(DicomTag.PatientID, new string[] { null }); - - var context = new FhirTransactionContext(ChangeFeedGenerator.Generate(metadata: dicomDataset)); - - await Assert.ThrowsAsync(() => _patientPipeline.PrepareRequestAsync(context, DefaultCancellationToken)); - } - - [Fact] - public async Task GivenNoExistingPatient_WhenRequestIsPrepared_ThenCorrectEntryComponentShouldBeCreated() - { - FhirTransactionContext context = CreateFhirTransactionContext(); - - Patient creatingPatient = null; - - _patientSynchronizer.When(async synchronizer => await synchronizer.SynchronizeAsync(context, Arg.Any(), isNewPatient: true, DefaultCancellationToken)).Do(callback => - { - creatingPatient = callback.ArgAt(1); - - // Modify a property of patient so changes can be detected. - creatingPatient.BirthDateElement = new Date(1990, 01, 01); - }); - - await _patientPipeline.PrepareRequestAsync(context, DefaultCancellationToken); - - FhirTransactionRequestEntry actualPatientEntry = context.Request.Patient; - - ValidationUtility.ValidateRequestEntryMinimumRequirementForWithChange(FhirTransactionRequestMode.Create, "Patient", Bundle.HTTPVerb.POST, actualPatientEntry); - - Assert.Equal("identifier=patientSystemId|p1", actualPatientEntry.Request.IfNoneExist); - - Patient actualPatient = Assert.IsType(actualPatientEntry.Resource); - - Assert.Collection( - actualPatient.Identifier, - identifier => ValidationUtility.ValidateIdentifier(DefaultPatientSystemId, DefaultPatientId, identifier)); - - Assert.Equal(creatingPatient.BirthDate, actualPatient.BirthDate); - } - - [Fact] - public async Task GivenExistingPatientAndHasChange_WhenRequestIsPrepared_ThenCorrectEntryComponentShouldBeCreated() - { - FhirTransactionContext context = CreateFhirTransactionContext(); - - _patientConfiguration = new PatientConfiguration() { PatientSystemId = "patientSystemId", IsIssuerIdUsed = true }; - IOptions optionsConfiguration = Options.Create(_patientConfiguration); - - var patient = new Patient() - { - Id = "patient1", - Meta = new Meta() - { - VersionId = "v1", - }, - }; - - _fhirService.RetrievePatientAsync(Arg.Is(TestUtility.BuildIdentifierPredicate(DefaultPatientSystemId, DefaultPatientId)), DefaultCancellationToken) - .Returns(patient); - - Patient updatingPatient = null; - - _patientSynchronizer.When(async synchronizer => await synchronizer.SynchronizeAsync(context, Arg.Any(), isNewPatient: false, DefaultCancellationToken)).Do(callback => - { - updatingPatient = callback.ArgAt(1); - - // Modify a property of patient so changes can be detected. - updatingPatient.Gender = AdministrativeGender.Other; - }); - - await _patientPipeline.PrepareRequestAsync(context, DefaultCancellationToken); - - FhirTransactionRequestEntry actualPatientEntry = context.Request.Patient; - - ValidationUtility.ValidateRequestEntryMinimumRequirementForWithChange(FhirTransactionRequestMode.Update, "Patient/patient1", Bundle.HTTPVerb.PUT, actualPatientEntry); - - Assert.Equal("W/\"v1\"", actualPatientEntry.Request.IfMatch); - Assert.Same(updatingPatient, actualPatientEntry.Resource); - } - - [Fact] - public async Task GivenExistingPatientAndNoChange_WhenRequestIsPrepared_ThenCorrectEntryComponentShouldBeCreated() - { - FhirTransactionContext context = CreateFhirTransactionContext(); - - var patient = new Patient() - { - Id = "patient1", - Meta = new Meta() - { - VersionId = "v1", - }, - }; - - _fhirService.RetrievePatientAsync(Arg.Is(TestUtility.BuildIdentifierPredicate(DefaultPatientSystemId, DefaultPatientId)), DefaultCancellationToken) - .Returns(patient); - - await _patientPipeline.PrepareRequestAsync(context, DefaultCancellationToken); - - FhirTransactionRequestEntry actualPatientEntry = context.Request.Patient; - - ValidationUtility.ValidateRequestEntryMinimumRequirementForNoChange(patient.ToServerResourceId(), actualPatientEntry); - } - - [Fact] - public void GivenNoUpdateToExistingPatient_WhenResponseIsProcessed_ThenItShouldBeNoOp() - { - // Simulate there is no update to patient resource (and therefore no response). - var context = new FhirTransactionContext(ChangeFeedGenerator.Generate()); - - context.Response.Patient = null; - - _patientPipeline.ProcessResponse(context); - } - - [Fact] - public void GivenARequestToCreateAPatient_WhenResponseIsOK_ThenResourceConflictExceptionShouldBeThrown() - { - var response = new Bundle.ResponseComponent(); - - response.AddAnnotation(HttpStatusCode.OK); - - var context = new FhirTransactionContext(ChangeFeedGenerator.Generate()); - - context.Request.Patient = FhirTransactionRequestEntryGenerator.GenerateDefaultCreateRequestEntry(); - - context.Response.Patient = new FhirTransactionResponseEntry(response, new Patient()); - - Assert.Throws(() => _patientPipeline.ProcessResponse(context)); - } - - [Fact] - public void GivenARequestToUpdateAPatient_WhenResponseIsOK_ThenItShouldBeNoOp() - { - var response = new Bundle.ResponseComponent(); - - response.AddAnnotation(HttpStatusCode.OK); - - var context = new FhirTransactionContext(ChangeFeedGenerator.Generate()); - - context.Request.Patient = FhirTransactionRequestEntryGenerator.GenerateDefaultUpdateRequestEntry( - new ServerResourceId(ResourceType.Patient, "123")); - - context.Response.Patient = new FhirTransactionResponseEntry(response, new Patient()); - - _patientPipeline.ProcessResponse(context); - } - - [Fact] - public async Task GivenIssuerIdFlagNotPresent_WhenResponseisOK_ThenPatientSystemIdShouldBePopulatedWithConfiguredValue() - { - FhirTransactionContext context = CreateFhirTransactionContext(); - - _patientConfiguration = new PatientConfiguration() { PatientSystemId = "patientSystemId" }; - IOptions optionsConfiguration = Options.Create(_patientConfiguration); - - _patientPipeline = new PatientPipelineStep(_fhirService, _patientSynchronizer, optionsConfiguration, NullLogger.Instance); - - await _patientPipeline.PrepareRequestAsync(context, DefaultCancellationToken); - - FhirTransactionRequestEntry actualPatientEntry = context.Request.Patient; - - Assert.Equal("identifier=patientSystemId|p1", actualPatientEntry.Request.IfNoneExist); - } - - [Fact] - public async Task GivenIssuerIdFlagDisabledAndPatientSystemIdNotConfigured_WhenResponseisOK_ThenPatientSystemIdShouldBeEmpty() - { - FhirTransactionContext context = CreateFhirTransactionContext(); - - _patientConfiguration = new PatientConfiguration() { IsIssuerIdUsed = false }; - IOptions optionsConfiguration = Options.Create(_patientConfiguration); - - _patientPipeline = new PatientPipelineStep(_fhirService, _patientSynchronizer, optionsConfiguration, NullLogger.Instance); - - await _patientPipeline.PrepareRequestAsync(context, DefaultCancellationToken); - - FhirTransactionRequestEntry actualPatientEntry = context.Request.Patient; - - Assert.Equal("identifier=|p1", actualPatientEntry.Request.IfNoneExist); - } - - [Fact] - public async Task GivenIssuerIdFlagEnabledAndTagPresent_WhenResponseisOK_ThenPatientSystemIdShouldBeIssuerId() - { - FhirTransactionContext context = CreateFhirTransactionContext(); - - _patientConfiguration = new PatientConfiguration() { PatientSystemId = "patientSystemId", IsIssuerIdUsed = true }; - IOptions optionsConfiguration = Options.Create(_patientConfiguration); - - _patientPipeline = new PatientPipelineStep(_fhirService, _patientSynchronizer, optionsConfiguration, NullLogger.Instance); - - context.ChangeFeedEntry.Metadata.Add(DicomTag.IssuerOfPatientID, "IssuerId"); - - await _patientPipeline.PrepareRequestAsync(context, DefaultCancellationToken); - - FhirTransactionRequestEntry actualPatientEntry = context.Request.Patient; - - Assert.Equal("identifier=IssuerId|p1", actualPatientEntry.Request.IfNoneExist); - } - - private static FhirTransactionContext CreateFhirTransactionContext() - { - return new FhirTransactionContext(ChangeFeedGenerator.Generate(metadata: DefaultDicomDataset)); - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/Patient/PatientSynchronizerTests.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/Patient/PatientSynchronizerTests.cs deleted file mode 100644 index 73e924fc8b..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/Patient/PatientSynchronizerTests.cs +++ /dev/null @@ -1,71 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Threading; -using FellowOakDicom; -using Hl7.Fhir.Model; -using Microsoft.Extensions.Options; -using Microsoft.Health.DicomCast.Core.Configurations; -using Microsoft.Health.DicomCast.Core.Features.ExceptionStorage; -using Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; -using NSubstitute; -using Xunit; -using Task = System.Threading.Tasks.Task; - -namespace Microsoft.Health.DicomCast.Core.UnitTests.Features.Worker.FhirTransaction; - -public class PatientSynchronizerTests -{ - private static readonly CancellationToken DefaultCancellationToken = new CancellationTokenSource().Token; - - private static readonly DicomDataset DefaultDicomDataset = new DicomDataset(); - - private readonly IPatientPropertySynchronizer _propertySynchronizer = Substitute.For(); - - private readonly DicomCastConfiguration _dicomCastConfig = new DicomCastConfiguration(); - - private readonly IExceptionStore _exceptionStore = Substitute.For(); - - [Fact] - public async Task WhenEnforceAllFields_AndError_ThrowsError() - { - _dicomCastConfig.Features.EnforceValidationOfTagValues = true; - - _propertySynchronizer.When(synchronizer => synchronizer.Synchronize(Arg.Any(), Arg.Any(), isNewPatient: false)).Do(synchronizer => { throw new InvalidDicomTagValueException("invalid tag", "invalid tag"); }); - - IEnumerable patientPropertySynchronizers = new List() - { - _propertySynchronizer, - }; - - PatientSynchronizer patientSynchronizer = new PatientSynchronizer(patientPropertySynchronizers, Options.Create(_dicomCastConfig), _exceptionStore); - - FhirTransactionContext context = new FhirTransactionContext(ChangeFeedGenerator.Generate(metadata: DefaultDicomDataset)); - var patient = new Patient(); - - await Assert.ThrowsAsync(() => patientSynchronizer.SynchronizeAsync(context, patient, false, DefaultCancellationToken)); - } - - [Fact] - public async Task WhenDoesNotEnforceAllFields_AndPropertyNotRequired_DoesNotThrowError() - { - _dicomCastConfig.Features.EnforceValidationOfTagValues = false; - - _propertySynchronizer.When(synchronizer => synchronizer.Synchronize(Arg.Any(), Arg.Any(), isNewPatient: false)).Do(synchronizer => { throw new InvalidDicomTagValueException("invalid tag", "invalid tag"); }); - - IEnumerable patientPropertySynchronizers = new List() - { - _propertySynchronizer, - }; - - PatientSynchronizer patientSynchronizer = new PatientSynchronizer(patientPropertySynchronizers, Options.Create(_dicomCastConfig), _exceptionStore); - - FhirTransactionContext context = new FhirTransactionContext(ChangeFeedGenerator.Generate(metadata: DefaultDicomDataset)); - var patient = new Patient(); - - await patientSynchronizer.SynchronizeAsync(context, patient, false, DefaultCancellationToken); - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/ResourceId/ClientResourceIdTests.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/ResourceId/ClientResourceIdTests.cs deleted file mode 100644 index 0f95e59dfd..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/ResourceId/ClientResourceIdTests.cs +++ /dev/null @@ -1,87 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using Hl7.Fhir.Model; -using Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; -using Xunit; - -namespace Microsoft.Health.DicomCast.Core.UnitTests.Features.Worker.FhirTransaction.ResourceId; - -public class ClientResourceIdTests -{ - private const string Prefix = "urn:uuid:"; - - private readonly ClientResourceId _clientResourceId = new ClientResourceId(); - - [Fact] - public void GivenTheSystem_WhenClientIdIsGenerated_ThenIdShouldHaveCorrectFormat() - { - Assert.StartsWith(Prefix, _clientResourceId.Id); - - string guid = _clientResourceId.Id.Substring(Prefix.Length); - - Assert.True(Guid.TryParse(guid, out Guid _)); - } - - [Fact] - public void GivenAClientResourceId_WhenConvertedToResourceReference_ThenCorrectResourceReferenceShouldBeCreated() - { - ResourceReference resourceReference = _clientResourceId.ToResourceReference(); - - Assert.NotNull(resourceReference); - Assert.Equal(_clientResourceId.Id, resourceReference.Reference); - } - - [Fact] - public void GivenDifferentClientResourceId_WhenHashCodeIsComputed_ThenHashCodeShouldBeDifferent() - { - var newClientResourceId = new ClientResourceId(); - - Assert.NotEqual(_clientResourceId.GetHashCode(), newClientResourceId.GetHashCode()); - } - - [Fact] - public void GivenAClientResourceId_WhenCheckingEqualToNullUsingObjectEquals_ThenFalseShouldBeReturned() - { - Assert.False(_clientResourceId.Equals((object)null)); - } - - [Fact] - public void GivenAClientResourceId_WhenCheckingEqualToSameClientResourceIdUsingObjectEquals_ThenTrueShouldBeReturned() - { - Assert.True(_clientResourceId.Equals((object)_clientResourceId)); - } - - [Fact] - public void GivenAClientResourceId_WhenCheckingEqualToDifferentClientResourceIdUsingObjectEquals_ThenFalseShouldBeReturned() - { - Assert.False(_clientResourceId.Equals((object)new ClientResourceId())); - } - - [Fact] - public void GivenAClientResourceId_WhenCheckingEqualToNullUsingIEquatableEquals_ThenFalseShouldBeReturned() - { - Assert.False(_clientResourceId.Equals(null)); - } - - [Fact] - public void GivenAClientResourceId_WhenCheckingEqualToSameClientResourceIdUsingIEquatableEquals_ThenTrueShouldBeReturned() - { - Assert.True(_clientResourceId.Equals(_clientResourceId)); - } - - [Fact] - public void GivenAClientResourceId_WhenCheckingEqualToDifferentClientResourceIdUsingIEquatableEquals_ThenFalseShouldBeReturned() - { - Assert.False(_clientResourceId.Equals(new ClientResourceId())); - } - - [Fact] - public void GivenAClientResourceId_WhenConvertedToString_ThenCorrectValueShouldBeReturned() - { - Assert.Equal(_clientResourceId.Id, _clientResourceId.ToString()); - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/ResourceId/ServerResourceIdTests.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/ResourceId/ServerResourceIdTests.cs deleted file mode 100644 index 3cd7869ae7..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/ResourceId/ServerResourceIdTests.cs +++ /dev/null @@ -1,117 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using Hl7.Fhir.Model; -using Hl7.Fhir.Utility; -using Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; -using Xunit; - -namespace Microsoft.Health.DicomCast.Core.UnitTests.Features.Worker.FhirTransaction.ResourceId; - -public class ServerResourceIdTests -{ - private const ResourceType DefaultResourceType = ResourceType.Patient; - private const string DefaultResourceId = "r123"; - private const string ResourceReference = "Patient/r123"; - - private static readonly ServerResourceId ServerResourceId = new ServerResourceId(DefaultResourceType, DefaultResourceId); - - public static IEnumerable GetDifferentServerResourceIdCombinations() - { - yield return new object[] { ResourceType.Practitioner, DefaultResourceId }; - yield return new object[] { DefaultResourceType, "r12" }; - yield return new object[] { DefaultResourceType, "R123" }; - } - - [Fact] - public void GivenTheSystem_WhenNewServerResourceIdIsGenerated_ThenCorrectValuesShouldBeSet() - { - Assert.Equal(EnumUtility.GetLiteral(ResourceType.Patient), ServerResourceId.TypeName); - Assert.Equal(DefaultResourceId, ServerResourceId.ResourceId); - } - - [Fact] - public void GivenAServerResourceId_WhenConvertedToResourceReference_ThenCorrectResourceReferenceShouldBeCreated() - { - ResourceReference resourceReference = ServerResourceId.ToResourceReference(); - - Assert.NotNull(resourceReference); - Assert.Equal(ResourceReference, resourceReference.Reference); - } - - [Fact] - public void GivenSameServerResourceId_WhenHashCodeIsComputed_ThenTheSameHashCodeShouldBeGenerated() - { - var newServerResourceId = new ServerResourceId(DefaultResourceType, DefaultResourceId); - - Assert.Equal(ServerResourceId.GetHashCode(), newServerResourceId.GetHashCode()); - } - - [Theory] - [MemberData(nameof(GetDifferentServerResourceIdCombinations))] - public void GivenDifferentServerResourceId_WhenHashCodeIsComputed_ThenHashCodeShouldBeDifferent(ResourceType resourceType, string resourceId) - { - var newServerResourceId = new ServerResourceId(resourceType, resourceId); - - Assert.NotEqual(ServerResourceId.GetHashCode(), newServerResourceId.GetHashCode()); - } - - [Fact] - public void GivenAServerResourceId_WhenCheckingEqualToNullUsingObjectEquals_ThenFalseShouldBeReturned() - { - Assert.False(ServerResourceId.Equals((object)null)); - } - - [Fact] - public void GivenAServerResourceId_WhenCheckingEqualToSameServerResourceIdUsingObjectEquals_ThenFalseShouldBeReturned() - { - Assert.True(ServerResourceId.Equals((object)ServerResourceId)); - } - - [Fact] - public void GivenAServerResourceId_WhenCheckingEqualToSameServerResourceIdUsingObjectEquals_ThenTrueShouldBeReturned() - { - Assert.True(ServerResourceId.Equals((object)new ServerResourceId(DefaultResourceType, DefaultResourceId))); - } - - [Theory] - [MemberData(nameof(GetDifferentServerResourceIdCombinations))] - public void GivenAServerResourceId_WhenCheckingEqualToDifferentServerResourceIdUsingObjectEquals_ThenFalseShouldBeReturned(ResourceType resourceType, string resourceId) - { - Assert.False(ServerResourceId.Equals((object)new ServerResourceId(resourceType, resourceId))); - } - - [Fact] - public void GivenAServerResourceId_WhenCheckingEqualToNullUsingIEquatableEquals_ThenFalseShouldBeReturned() - { - Assert.False(ServerResourceId.Equals(null)); - } - - [Fact] - public void GivenAServerResourceId_WhenCheckingEqualToSameServerResourceIdUsingIEquatableEquals_ThenFalseShouldBeReturned() - { - Assert.True(ServerResourceId.Equals(ServerResourceId)); - } - - [Fact] - public void GivenAServerResourceId_WhenCheckingEqualToSameServerResourceIdUsingIEquatableEquals_ThenTrueShouldBeReturned() - { - Assert.True(ServerResourceId.Equals(new ServerResourceId(DefaultResourceType, DefaultResourceId))); - } - - [Theory] - [MemberData(nameof(GetDifferentServerResourceIdCombinations))] - public void GivenAServerResourceId_WhenCheckingEqualToDifferentServerResourceIdUsingIEquatableEquals_ThenFalseShouldBeReturned(ResourceType resourceType, string resourceId) - { - Assert.False(ServerResourceId.Equals(new ServerResourceId(resourceType, resourceId))); - } - - [Fact] - public void GivenAServerResourceId_WhenConvertedToString_ThenCorrectValueShouldBeReturned() - { - Assert.Equal(ResourceReference, ServerResourceId.ToString()); - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/TestUtility.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/TestUtility.cs deleted file mode 100644 index be663e6814..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/TestUtility.cs +++ /dev/null @@ -1,38 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Globalization; -using System.Linq.Expressions; -using FellowOakDicom; -using Hl7.Fhir.Model; -using Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -namespace Microsoft.Health.DicomCast.Core.UnitTests.Features.Worker.FhirTransaction; - -public static class TestUtility -{ - public static Expression> BuildIdentifierPredicate(string system, string value) - => identifier => identifier != null && - string.Equals(identifier.System, system, StringComparison.Ordinal) && - string.Equals(identifier.Value, value, StringComparison.Ordinal); - - public static TimeSpan SetDateTimeOffSet(DicomDataset metadata) - { - if (metadata.TryGetSingleValue(DicomTag.TimezoneOffsetFromUTC, out string utcOffsetInString)) - { - try - { - return DateTimeOffset.ParseExact(utcOffsetInString, "zzz", CultureInfo.InvariantCulture, DateTimeStyles.AllowWhiteSpaces).Offset; - } - catch (FormatException) - { - throw new InvalidDicomTagValueException(nameof(DicomTag.TimezoneOffsetFromUTC), utcOffsetInString); - } - } - - return TimeSpan.Zero; - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/ValidationUtility.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/ValidationUtility.cs deleted file mode 100644 index 01c68cf03a..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Features/Worker/FhirTransaction/ValidationUtility.cs +++ /dev/null @@ -1,119 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using Hl7.Fhir.Model; -using Microsoft.Health.DicomCast.Core.Features.Fhir; -using Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; -using Xunit; - -namespace Microsoft.Health.DicomCast.Core.UnitTests.Features.Worker.FhirTransaction; - -public static class ValidationUtility -{ - public static void ValidateRequestEntryMinimumRequirementForWithChange( - FhirTransactionRequestMode expectedRequestMode, - string path, - Bundle.HTTPVerb? expectedMethod, - FhirTransactionRequestEntry actualEntry) - { - // For request entry with no change, use the method below. - Assert.NotEqual(FhirTransactionRequestMode.None, expectedRequestMode); - - Assert.NotNull(actualEntry); - Assert.Equal(expectedRequestMode, actualEntry.RequestMode); - - if (expectedRequestMode == FhirTransactionRequestMode.Create) - { - // If the request mode is create, then it should be client generated resource id. - Assert.IsType(actualEntry.ResourceId); - } - else if (expectedRequestMode == FhirTransactionRequestMode.Update) - { - // Otherwise, it should be server generated resource id. - ServerResourceId serverResourceId = Assert.IsType(actualEntry.ResourceId); - - Assert.Equal(path, serverResourceId.ToString()); - } - - Assert.NotNull(actualEntry.Request); - Assert.Equal(expectedMethod, actualEntry.Request.Method); - Assert.Equal(path, actualEntry.Request.Url); - - if (expectedMethod != Bundle.HTTPVerb.DELETE) - { - Assert.NotNull(actualEntry.Resource); - } - } - - public static void ValidateRequestEntryMinimumRequirementForNoChange(ServerResourceId expectedResourceId, FhirTransactionRequestEntry actualEntry) - { - Assert.NotNull(actualEntry); - Assert.Equal(FhirTransactionRequestMode.None, actualEntry.RequestMode); - - // No update means the resource already exists and nothing has changed, - // so it should still have server generated resource id. - ServerResourceId serverResourceId = Assert.IsType(actualEntry.ResourceId); - - Assert.Equal(expectedResourceId, serverResourceId); - - Assert.Null(actualEntry.Request); - } - - public static void ValidateIdentifier(string expectedSystem, string expectedValue, Identifier actualIdentifier) - { - Assert.NotNull(actualIdentifier); - Assert.Equal(expectedSystem, actualIdentifier.System); - Assert.Equal(expectedValue, actualIdentifier.Value); - } - - public static void ValidateAccessionNumber(string expectedSystem, string expectedValue, Identifier actualIdentifier) - { - Assert.NotNull(actualIdentifier); - Assert.Equal(actualIdentifier.Value, expectedValue); - Assert.Equal(actualIdentifier.System, expectedSystem); - Assert.Single(actualIdentifier.Type.Coding); - Assert.Equal("http://terminology.hl7.org/CodeSystem/v2-0203", actualIdentifier.Type.Coding[0].System); - Assert.Equal("ACSN", actualIdentifier.Type.Coding[0].Code); - } - - public static void ValidateResourceReference(string expectedReference, ResourceReference actualResourceReference) - { - Assert.NotNull(actualResourceReference); - Assert.Equal(expectedReference, actualResourceReference.Reference); - } - - public static void ValidateSeries(ImagingStudy.SeriesComponent series, string seriesInstanceUid, params string[] sopInstanceUidList) - { - Assert.Equal(seriesInstanceUid, series.Uid); - - for (int i = 0; i < series.Instance.Count; i++) - { - Assert.Equal(sopInstanceUidList[i], series.Instance[i].Uid); - } - } - - public static ImagingStudy ValidateImagingStudyUpdate(string studyInstanceUid, string patientResourceId, FhirTransactionRequestEntry entry, bool hasAccessionNumber = true) - { - Identifier expectedIdentifier = IdentifierUtility.CreateIdentifier(studyInstanceUid); - string expectedRequestUrl = $"ImagingStudy/{entry.Resource.Id}"; - - ValidateRequestEntryMinimumRequirementForWithChange(FhirTransactionRequestMode.Update, expectedRequestUrl, Bundle.HTTPVerb.PUT, actualEntry: entry); - - ImagingStudy updatedImagingStudy = Assert.IsType(entry.Resource); - - Assert.Equal(ImagingStudy.ImagingStudyStatus.Available, updatedImagingStudy.Status); - - ValidateResourceReference(patientResourceId, updatedImagingStudy.Subject); - - Action studyIdValidaion = identifier => ValidateIdentifier("urn:dicom:uid", $"urn:oid:{studyInstanceUid}", identifier); - Action accessionNumberValidation = identifier => ValidateAccessionNumber(null, FhirTransactionContextBuilder.DefaultAccessionNumber, identifier); - Assert.Collection( - updatedImagingStudy.Identifier, - hasAccessionNumber ? new[] { studyIdValidaion, accessionNumberValidation } : new[] { studyIdValidaion }); - - return updatedImagingStudy; - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Microsoft.Health.DicomCast.Core.UnitTests.csproj b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Microsoft.Health.DicomCast.Core.UnitTests.csproj deleted file mode 100644 index 8975e6f811..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core.UnitTests/Microsoft.Health.DicomCast.Core.UnitTests.csproj +++ /dev/null @@ -1,34 +0,0 @@ - - - - $(LatestVersion) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Configurations/DicomCastConfiguration.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Configurations/DicomCastConfiguration.cs deleted file mode 100644 index e19eba7d3a..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Configurations/DicomCastConfiguration.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.DicomCast.Core.Configurations; - -/// -/// Configuration for DicomCast -/// -public class DicomCastConfiguration -{ - public FeatureConfiguration Features { get; } = new FeatureConfiguration(); -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Configurations/DicomCastWorkerConfiguration.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Configurations/DicomCastWorkerConfiguration.cs deleted file mode 100644 index a0f5dafa10..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Configurations/DicomCastWorkerConfiguration.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; - -namespace Microsoft.Health.DicomCast.Core.Configurations; - -/// -/// The configuration related to . -/// -public class DicomCastWorkerConfiguration -{ - /// - /// The period of time to wait before polling new changes feed from DICOMWeb when previous poll indicates there are no more new changes. - /// - public TimeSpan PollInterval { get; set; } = TimeSpan.FromMinutes(1); - - /// - /// The period of time to wait before polling new changes feed from DICOMWeb when previous poll indicates there are potentially new changes. - /// - public TimeSpan PollIntervalDuringCatchup { get; set; } = TimeSpan.Zero; -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Configurations/DicomWebConfiguration.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Configurations/DicomWebConfiguration.cs deleted file mode 100644 index b371d3d4fa..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Configurations/DicomWebConfiguration.cs +++ /dev/null @@ -1,34 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using Microsoft.Health.Client.Authentication; - -namespace Microsoft.Health.DicomCast.Core.Configurations; - -/// -/// The configuration related to DICOMWeb service. -/// -public class DicomWebConfiguration -{ - /// - /// The endpoint to DICOMWeb service. - /// - public Uri Endpoint { get; set; } - - /// - /// The optional private endpoint to use to talk to dicom - /// - /// - /// If this url is specified then it will be used to talk to dicom, but it will not be used when specifying the url in the fhir objects. - /// The value of will still be used to generate links to dicom objects in fhir. - /// - public Uri PrivateEndpoint { get; set; } - - /// - /// Authentication settings for DICOMWeb. - /// - public AuthenticationOptions Authentication { get; set; } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Configurations/FeatureConfiguration.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Configurations/FeatureConfiguration.cs deleted file mode 100644 index 9a1660b94d..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Configurations/FeatureConfiguration.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.DicomCast.Core.Configurations; - -public class FeatureConfiguration -{ - /// - /// Do not sync values that are invalid and are not required - /// - public bool EnforceValidationOfTagValues { get; set; } - - /// - /// Generate observations from dcm - /// - public bool GenerateObservations { get; set; } - - /// - /// Ignore json parsing errors from DicomWebClient - /// - public bool IgnoreJsonParsingErrors { get; set; } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Configurations/FhirConfiguration.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Configurations/FhirConfiguration.cs deleted file mode 100644 index 070d93fc15..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Configurations/FhirConfiguration.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using Microsoft.Health.Client.Authentication; - -namespace Microsoft.Health.DicomCast.Core.Configurations; - -public class FhirConfiguration -{ - public Uri Endpoint { get; set; } - - /// - /// Authentication settings for the FHIR server. - /// - public AuthenticationOptions Authentication { get; set; } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Configurations/PatientConfiguration.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Configurations/PatientConfiguration.cs deleted file mode 100644 index dfffa18ec1..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Configurations/PatientConfiguration.cs +++ /dev/null @@ -1,25 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.DicomCast.Core.Configurations; - -/// -/// Configuration for Patient system Id -/// -public sealed class PatientConfiguration -{ - /// - /// Patient System Id configured by the user - /// - public string PatientSystemId { get; set; } - - /// - /// Issuer Id or Patient System Id used based on this boolean value - /// - /// - /// If the IsIssuerIdUsed flag is set to true, the value from Issue of Patient Id would override the patient system id - /// - public bool IsIssuerIdUsed { get; set; } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Configurations/RetryConfiguration.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Configurations/RetryConfiguration.cs deleted file mode 100644 index e2ee290664..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Configurations/RetryConfiguration.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; - -namespace Microsoft.Health.DicomCast.Core.Configurations; - -/// -/// The configuration for retryable exceptions. -/// -public class RetryConfiguration -{ - /// - /// The total amount of time to spend retrying a single change feed entry across all retries. - /// - public TimeSpan TotalRetryDuration { get; set; } = TimeSpan.FromMinutes(10); -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/DicomCastCoreResource.Designer.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/DicomCastCoreResource.Designer.cs deleted file mode 100644 index c442ec485e..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/DicomCastCoreResource.Designer.cs +++ /dev/null @@ -1,252 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Microsoft.Health.DicomCast.Core { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class DicomCastCoreResource { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal DicomCastCoreResource() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.Health.DicomCast.Core.DicomCastCoreResource", typeof(DicomCastCoreResource).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to The data store operation failed.. - /// - internal static string DataStoreOperationFailed { - get { - return ResourceManager.GetString("DataStoreOperationFailed", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to FHIR server version cannot be validated. Configure a FHIR service with version R4 and with transaction capabilites.. - /// - internal static string FailedToValidateFhirVersion { - get { - return ResourceManager.GetString("FailedToValidateFhirVersion", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Configured FHIR service does not support transactions needed to publish FHIR changes. Configure a FHIR service with transaction capabilites.. - /// - internal static string FhirServerTransactionNotSupported { - get { - return ResourceManager.GetString("FhirServerTransactionNotSupported", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The response status value '{0}' for bundle entry at index '{1}' is invalid.. - /// - internal static string InvalidBundleEntryResponseStatus { - get { - return ResourceManager.GetString("InvalidBundleEntryResponseStatus", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The value '{0}' is not valid for DICOM tag '{1}'.. - /// - internal static string InvalidDicomTagValue { - get { - return ResourceManager.GetString("InvalidDicomTagValue", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The id of the resource is missing.. - /// - internal static string InvalidFhirResourceMissingId { - get { - return ResourceManager.GetString("InvalidFhirResourceMissingId", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The version id of the resource is missing.. - /// - internal static string InvalidFhirResourceMissingVersionId { - get { - return ResourceManager.GetString("InvalidFhirResourceMissingVersionId", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to FHIR service version is not supported. Configure a FHIR service that supports FHIR version R4.. - /// - internal static string InvalidFhirServerVersion { - get { - return ResourceManager.GetString("InvalidFhirServerVersion", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The number of bundle entry in response does not match the request.. - /// - internal static string MismatchBundleEntryCount { - get { - return ResourceManager.GetString("MismatchBundleEntryCount", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The endpoint already exists but the address does not match.. - /// - internal static string MismatchEndpointAddress { - get { - return ResourceManager.GetString("MismatchEndpointAddress", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The transaction indicates success but the status value '{0}' for bundle entry at index '{1}' indicates failure. . - /// - internal static string MismatchTransactionStatusCode { - get { - return ResourceManager.GetString("MismatchTransactionStatusCode", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The bundle entry at index '{0}' is missing.. - /// - internal static string MissingBundleEntry { - get { - return ResourceManager.GetString("MissingBundleEntry", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The response for bundle entry at index '{0}' is missing.. - /// - internal static string MissingBundleEntryResponse { - get { - return ResourceManager.GetString("MissingBundleEntryResponse", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The response status for bundle entry at index '{0}' is missing.. - /// - internal static string MissingBundleEntryResponseStatus { - get { - return ResourceManager.GetString("MissingBundleEntryResponseStatus", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The required DICOM tag '{0}' is missing.. - /// - internal static string MissingRequiredDicomTag { - get { - return ResourceManager.GetString("MissingRequiredDicomTag", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The response bundle is missing.. - /// - internal static string MissingResponseBundle { - get { - return ResourceManager.GetString("MissingResponseBundle", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Multiple '{0}' matching the search criteria found.. - /// - internal static string MultipleMatchingResourcesFound { - get { - return ResourceManager.GetString("MultipleMatchingResourcesFound", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The change feed action '{0}' is not supported.. - /// - internal static string NotSupportedChangeFeedAction { - get { - return ResourceManager.GetString("NotSupportedChangeFeedAction", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A retryable exception was thrown.. - /// - internal static string RetryableException { - get { - return ResourceManager.GetString("RetryableException", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The transaction failed. Please examine the OperationOutcome for more detail.. - /// - internal static string TransactionFailed { - get { - return ResourceManager.GetString("TransactionFailed", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The type name '{0}' is not a recognized resource type.. - /// - internal static string UnknownResourceType { - get { - return ResourceManager.GetString("UnknownResourceType", resourceCulture); - } - } - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/DicomCastCoreResource.resx b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/DicomCastCoreResource.resx deleted file mode 100644 index 8e4c921868..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/DicomCastCoreResource.resx +++ /dev/null @@ -1,193 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - The data store operation failed. - - - FHIR server version cannot be validated. Configure a FHIR service with version R4 and with transaction capabilites. - - - Configured FHIR service does not support transactions needed to publish FHIR changes. Configure a FHIR service with transaction capabilites. - - - The response status value '{0}' for bundle entry at index '{1}' is invalid. - {0} is the status value. {1} is the index. - - - The value '{0}' is not valid for DICOM tag '{1}'. - {0} is the supplied value. {1} is the tag name. - - - The id of the resource is missing. - - - The version id of the resource is missing. - - - FHIR service version is not supported. Configure a FHIR service that supports FHIR version R4. - - - The number of bundle entry in response does not match the request. - - - The endpoint already exists but the address does not match. - - - The transaction indicates success but the status value '{0}' for bundle entry at index '{1}' indicates failure. - {0} is the status value. {1} is the index. - - - The bundle entry at index '{0}' is missing. - {0} is the index. - - - The response for bundle entry at index '{0}' is missing. - {0} is the index. - - - The response status for bundle entry at index '{0}' is missing. - {0} is the index. - - - The required DICOM tag '{0}' is missing. - {0} is the tag name. - - - The response bundle is missing. - - - Multiple '{0}' matching the search criteria found. - {0} is the resource type. - - - The change feed action '{0}' is not supported. - {0} is the change feed action enum value. - - - A retryable exception was thrown. - - - The transaction failed. Please examine the OperationOutcome for more detail. - - - The type name '{0}' is not a recognized resource type. - {0} is the type name. - - \ No newline at end of file diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Exceptions/DataStoreException.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Exceptions/DataStoreException.cs deleted file mode 100644 index 66697fa6a9..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Exceptions/DataStoreException.cs +++ /dev/null @@ -1,16 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; - -namespace Microsoft.Health.DicomCast.Core.Exceptions; - -public class DataStoreException : Exception -{ - public DataStoreException(Exception innerException) - : base(DicomCastCoreResource.DataStoreOperationFailed, innerException) - { - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Exceptions/DicomTagException.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Exceptions/DicomTagException.cs deleted file mode 100644 index c169dddc1d..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Exceptions/DicomTagException.cs +++ /dev/null @@ -1,16 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; - -namespace Microsoft.Health.DicomCast.Core.Exceptions; - -public class DicomTagException : Exception -{ - public DicomTagException(string message) - : base(message) - { - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Exceptions/RetryableException.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Exceptions/RetryableException.cs deleted file mode 100644 index 254ce4469b..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Exceptions/RetryableException.cs +++ /dev/null @@ -1,20 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; - -namespace Microsoft.Health.DicomCast.Core.Exceptions; - -public class RetryableException : Exception -{ - public RetryableException() - { - } - - public RetryableException(Exception innerException) - : base(DicomCastCoreResource.RetryableException, innerException) - { - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Exceptions/ServerTooBusyException.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Exceptions/ServerTooBusyException.cs deleted file mode 100644 index d032e09f03..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Exceptions/ServerTooBusyException.cs +++ /dev/null @@ -1,16 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.DicomCast.Core.Exceptions; - -/// -/// Exception thrown when the server is too busy. -/// -public class ServerTooBusyException : RetryableException -{ - public ServerTooBusyException() - { - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Extensions/DicomDatasetExtensions.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Extensions/DicomDatasetExtensions.cs deleted file mode 100644 index 26a9c4bd38..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Extensions/DicomDatasetExtensions.cs +++ /dev/null @@ -1,55 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using EnsureThat; -using FellowOakDicom; -using Hl7.Fhir.Model; - -namespace Microsoft.Health.DicomCast.Core.Extensions; - -public static class DicomDatasetExtensions -{ - public static Date GetDatePropertyIfNotDefaultValue(this DicomDataset dataset, DicomTag dateDicomTag) - { - EnsureArg.IsNotNull(dataset, nameof(dataset)); - - if (dataset.TryGetSingleValue(dateDicomTag, out DateTime dateTagValue) && dateTagValue != DateTime.MinValue) - { - var fhirDate = new Date(dateTagValue.Year, dateTagValue.Month, dateTagValue.Day); - if (Date.IsValidValue(fhirDate.Value)) - { - return fhirDate; - } - } - - return null; - } - - public static FhirDateTime GetDateTimePropertyIfNotDefaultValue(this DicomDataset dataset, DicomTag dateDicomTag, DicomTag timeDicomTag, TimeSpan utcOffset) - { - EnsureArg.IsNotNull(dataset, nameof(dataset)); - - if (dataset.TryGetSingleValue(dateDicomTag, out DateTime studyDate) && dataset.TryGetSingleValue(timeDicomTag, out DateTime studyTime)) - { - if (studyDate != DateTime.MinValue || studyTime != DateTime.MinValue) - { - var studyDateTime = new DateTimeOffset( - studyDate.Year, - studyDate.Month, - studyDate.Day, - studyTime.Hour, - studyTime.Minute, - studyTime.Second, - studyTime.Millisecond, - utcOffset); - - return new FhirDateTime(studyDateTime); - } - } - - return null; - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Extensions/IdentifierExtensions.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Extensions/IdentifierExtensions.cs deleted file mode 100644 index a6aae00f85..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Extensions/IdentifierExtensions.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using Hl7.Fhir.Model; - -namespace Microsoft.Health.DicomCast.Core.Extensions; - -public static class IdentifierExtensions -{ - public static string ToSearchQueryParameter(this Identifier identifier) - { - EnsureArg.IsNotNull(identifier, nameof(identifier)); - - return $"identifier={identifier.System}|{identifier.Value}"; - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Extensions/ResourceExtensions.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Extensions/ResourceExtensions.cs deleted file mode 100644 index 49110172c0..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Extensions/ResourceExtensions.cs +++ /dev/null @@ -1,20 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using Hl7.Fhir.Model; -using Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -namespace Microsoft.Health.DicomCast.Core.Extensions; - -public static class ResourceExtensions -{ - public static ServerResourceId ToServerResourceId(this Resource resource) - { - EnsureArg.IsNotNull(resource, nameof(resource)); - - return new ServerResourceId(resource.TypeName, resource.Id); - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Extensions/ServiceCollectionExtensions.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Extensions/ServiceCollectionExtensions.cs deleted file mode 100644 index 4c3391adaf..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Extensions/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,33 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; - -namespace Microsoft.Health.DicomCast.Core.Extensions; - -public static class ServiceCollectionExtensions -{ - public static TConfiguration Configure( - this IServiceCollection services, - IConfiguration configuration, - string sectionName) - where TConfiguration : class, new() - { - EnsureArg.IsNotNull(services, nameof(services)); - EnsureArg.IsNotNull(configuration, nameof(configuration)); - EnsureArg.IsNotNullOrWhiteSpace(sectionName, nameof(sectionName)); - - var config = new TConfiguration(); - - configuration.GetSection(sectionName).Bind(config); - - services.AddSingleton(Options.Create(config)); - - return config; - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/DicomWeb/Service/ChangeFeedRetrieveService.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/DicomWeb/Service/ChangeFeedRetrieveService.cs deleted file mode 100644 index 2c76b62318..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/DicomWeb/Service/ChangeFeedRetrieveService.cs +++ /dev/null @@ -1,46 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.Health.Dicom.Client; -using Microsoft.Health.Dicom.Client.Models; - -namespace Microsoft.Health.DicomCast.Core.Features.DicomWeb.Service; - -/// -/// Provides functionality to retrieve the change feed from DICOMWeb. -/// -public class ChangeFeedRetrieveService : IChangeFeedRetrieveService -{ - private readonly IDicomWebClient _dicomWebClient; - - public ChangeFeedRetrieveService(IDicomWebClient dicomWebClient) - { - _dicomWebClient = EnsureArg.IsNotNull(dicomWebClient, nameof(dicomWebClient)); - } - - /// - public async Task> RetrieveChangeFeedAsync(long offset, int limit, CancellationToken cancellationToken = default) - { - using DicomWebAsyncEnumerableResponse result = await _dicomWebClient.GetChangeFeed( - $"?offset={offset}&limit={limit}&includeMetadata=true", - cancellationToken); - - return await result.ToArrayAsync(cancellationToken) ?? Array.Empty(); - } - - public async Task RetrieveLatestSequenceAsync(CancellationToken cancellationToken = default) - { - using DicomWebResponse response = await _dicomWebClient.GetChangeFeedLatest("?includeMetadata=false", cancellationToken); - ChangeFeedEntry latest = await response.GetValueAsync(); - - return latest?.Sequence ?? 0L; // 0L is the default offset used by the Change Feed and SyncStateStore - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/DicomWeb/Service/IChangeFeedRetrieveService.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/DicomWeb/Service/IChangeFeedRetrieveService.cs deleted file mode 100644 index 2f59c46f37..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/DicomWeb/Service/IChangeFeedRetrieveService.cs +++ /dev/null @@ -1,33 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Health.Dicom.Client.Models; - -namespace Microsoft.Health.DicomCast.Core.Features.DicomWeb.Service; - -/// -/// Provides functionality to retrieve the change feed from DICOMWeb. -/// -public interface IChangeFeedRetrieveService -{ - /// - /// Asynchronously retrieves the change feed. - /// - /// Skip events till sequence number. - /// The maximum number of events to fetch. - /// The cancellation token. - /// A task representing the retrieving operation. - Task> RetrieveChangeFeedAsync(long offset, int limit, CancellationToken cancellationToken = default); - - /// - /// Asynchronously retrieves the latest entry sequence number from the change feed. - /// - /// The cancellation token. - /// A task representing the retrieving operation. - Task RetrieveLatestSequenceAsync(CancellationToken cancellationToken = default); -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/ExceptionStorage/ErrorType.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/ExceptionStorage/ErrorType.cs deleted file mode 100644 index 4efb543e09..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/ExceptionStorage/ErrorType.cs +++ /dev/null @@ -1,32 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.DicomCast.Core.Features.ExceptionStorage; - -/// -/// Represents the type of error -/// -public enum ErrorType -{ - /// - /// A transient error that has reached the maximum number of retries - /// - TransientFailure, - - /// - /// An intransient error caused by fhir server that has caused a failure to sync to fhir - /// - FhirError, - - /// - /// An intransient error caused by an invalid dicom value that has caused a failure to sync to fhir - /// - DicomError, - - /// - /// An intransient error that occurered due to invalid data for a non required entry in the DICOM change feed entry. Does not mean failed to sync to fhir yet - /// - DicomValidationError, -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/ExceptionStorage/IExceptionStore.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/ExceptionStorage/IExceptionStore.cs deleted file mode 100644 index 7cd8cb85fe..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/ExceptionStorage/IExceptionStore.cs +++ /dev/null @@ -1,41 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Health.Dicom.Client.Models; - -namespace Microsoft.Health.DicomCast.Core.Features.ExceptionStorage; - -/// -/// Service that supports storing exceptions. -/// -public interface IExceptionStore -{ - /// - /// Store an exception to an azure storage table. - /// - /// ChangeFeedEntry that threw exception - /// The exception that was thrown and needs to be stored - /// The type of error thrown - /// Cancellation token. - Task WriteExceptionAsync(ChangeFeedEntry changeFeedEntry, Exception exceptionToStore, ErrorType errorType, CancellationToken cancellationToken = default); - - /// - /// Store a retryrable exception to an azure storage table. - /// - /// ChangeFeedEntry that threw exception - /// Number of times the entry has been tried - /// TimeSpan to wait before next retry - /// The exception that was thrown and needs to be stored - /// Cancellation token. - Task WriteRetryableExceptionAsync(ChangeFeedEntry changeFeedEntry, int retryNum, TimeSpan nextDelayTimeSpan, Exception exceptionToStore, CancellationToken cancellationToken = default); - - Task<(IEnumerable results, string continuationToken)> ReadIntransientErrors(ErrorType errorType, string continuationToken, CancellationToken cancellationToken = default); - - Task<(IEnumerable results, string continuationToken)> ReadRetryableErrors(ErrorType errorType, string continuationToken, CancellationToken cancellationToken = default); -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/ExceptionStorage/IntransientError.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/ExceptionStorage/IntransientError.cs deleted file mode 100644 index f41c28009b..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/ExceptionStorage/IntransientError.cs +++ /dev/null @@ -1,48 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using EnsureThat; - -namespace Microsoft.Health.DicomCast.Core.Features.ExceptionStorage; - -public class IntransientError -{ - public IntransientError() - { - } - - /// - /// Initializes a new instance of the class. - /// - /// StudyUid of the changefeed entry that failed - /// SeriesUid of the changefeed entry that failed - /// InstanceUid of the changefeed entry that failed - /// Changefeed sequence number that threw exception - /// The exception that was thrown - public IntransientError(string studyUid, string seriesUid, string instanceUid, long changeFeedSequence, Exception ex) - { - EnsureArg.IsNotNull(studyUid, nameof(studyUid)); - EnsureArg.IsNotNull(seriesUid, nameof(seriesUid)); - EnsureArg.IsNotNull(instanceUid, nameof(instanceUid)); - EnsureArg.IsNotNull(ex, nameof(ex)); - - StudyUid = studyUid; - SeriesUid = seriesUid; - InstanceUid = instanceUid; - ChangeFeedSequence = changeFeedSequence; - Exception = ex.ToString(); - } - - public string StudyUid { get; set; } - - public string SeriesUid { get; set; } - - public string InstanceUid { get; set; } - - public string Exception { get; set; } - - public long ChangeFeedSequence { get; set; } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/ExceptionStorage/RetryableError.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/ExceptionStorage/RetryableError.cs deleted file mode 100644 index d610382c1b..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/ExceptionStorage/RetryableError.cs +++ /dev/null @@ -1,52 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using EnsureThat; - -namespace Microsoft.Health.DicomCast.Core.Features.ExceptionStorage; - -public class RetryableError -{ - public RetryableError() - { - } - - /// - /// Initializes a new instance of the class. - /// - /// StudyUid of the changefeed entry that failed - /// SeriesUid of the changefeed entry that failed - /// InstanceUid of the changefeed entry that failed - /// Changefeed sequence number that threw exception - /// Number of times changefeed entry has been retried - /// The exception that was thrown - public RetryableError(string studyUid, string seriesUid, string instanceUid, long changeFeedSequence, int retryNum, Exception ex) - { - EnsureArg.IsNotNull(studyUid, nameof(studyUid)); - EnsureArg.IsNotNull(seriesUid, nameof(seriesUid)); - EnsureArg.IsNotNull(instanceUid, nameof(instanceUid)); - EnsureArg.IsNotNull(ex, nameof(ex)); - - StudyUid = studyUid; - SeriesUid = seriesUid; - InstanceUid = instanceUid; - ChangeFeedSequence = changeFeedSequence; - RetryNumber = retryNum; - Exception = ex.ToString(); - } - - public string StudyUid { get; set; } - - public string SeriesUid { get; set; } - - public string InstanceUid { get; set; } - - public long ChangeFeedSequence { get; set; } - - public int RetryNumber { get; set; } - - public string Exception { get; set; } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Fhir/FhirNonRetryableException.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Fhir/FhirNonRetryableException.cs deleted file mode 100644 index 349c9e4b33..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Fhir/FhirNonRetryableException.cs +++ /dev/null @@ -1,21 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; - -namespace Microsoft.Health.DicomCast.Core.Features.Fhir; - -public class FhirNonRetryableException : Exception -{ - public FhirNonRetryableException(string message) - : base(message) - { - } - - public FhirNonRetryableException(string message, Exception innerException) - : base(message, innerException) - { - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Fhir/FhirResourceValidationException.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Fhir/FhirResourceValidationException.cs deleted file mode 100644 index 8199ab1573..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Fhir/FhirResourceValidationException.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.DicomCast.Core.Features.Fhir; - -/// -/// Exception thrown when a fails validation. -/// -public class FhirResourceValidationException : FhirNonRetryableException -{ - public FhirResourceValidationException(string message) - : base(message) - { - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Fhir/FhirResourceValidator.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Fhir/FhirResourceValidator.cs deleted file mode 100644 index b3d8b978df..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Fhir/FhirResourceValidator.cs +++ /dev/null @@ -1,31 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using Hl7.Fhir.Model; - -namespace Microsoft.Health.DicomCast.Core.Features.Fhir; - -/// -/// Provides functionality to validate a resource. -/// -public class FhirResourceValidator : IFhirResourceValidator -{ - /// - public void Validate(Resource resource) - { - EnsureArg.IsNotNull(resource, nameof(resource)); - - if (string.IsNullOrWhiteSpace(resource.Id)) - { - throw new FhirResourceValidationException(DicomCastCoreResource.InvalidFhirResourceMissingId); - } - - if (string.IsNullOrWhiteSpace(resource.Meta?.VersionId)) - { - throw new FhirResourceValidationException(DicomCastCoreResource.InvalidFhirResourceMissingVersionId); - } - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Fhir/FhirService.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Fhir/FhirService.cs deleted file mode 100644 index 2be7a5005c..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Fhir/FhirService.cs +++ /dev/null @@ -1,147 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Hl7.Fhir.Model; -using Microsoft.Health.DicomCast.Core.Extensions; -using Microsoft.Health.Fhir.Client; -using static Hl7.Fhir.Model.CapabilityStatement; -using IFhirClient = Microsoft.Health.Fhir.Client.IFhirClient; -using Task = System.Threading.Tasks.Task; - -namespace Microsoft.Health.DicomCast.Core.Features.Fhir; - -/// -/// Provides functionalities to communicate with FHIR server. -/// -public class FhirService : IFhirService -{ - private readonly IFhirClient _fhirClient; - private readonly IFhirResourceValidator _fhirResourceValidator; - - private readonly IEnumerable _supportedFHIRVersions = new List { FHIRVersion.N4_0_0, FHIRVersion.N4_0_1 }; - - public FhirService(IFhirClient fhirClient, IFhirResourceValidator fhirResourceValidator) - { - EnsureArg.IsNotNull(fhirClient, nameof(fhirClient)); - EnsureArg.IsNotNull(fhirResourceValidator, nameof(fhirResourceValidator)); - - _fhirClient = fhirClient; - _fhirResourceValidator = fhirResourceValidator; - } - - /// - public Task RetrievePatientAsync(Identifier identifier, CancellationToken cancellationToken) - => SearchByIdentifierAsync(identifier, cancellationToken); - - /// - public Task RetrieveImagingStudyAsync(Identifier identifier, CancellationToken cancellationToken) - => SearchByIdentifierAsync(identifier, cancellationToken); - - /// - public async Task RetrieveEndpointAsync(string queryParameter, CancellationToken cancellationToken) - => (await SearchByQueryParameterAsync(queryParameter, 1, cancellationToken)).FirstOrDefault(); - - /// - public Task> RetrieveObservationsAsync(Identifier identifier, CancellationToken cancellationToken) - => SearchByIdentifierMultipleAsync(identifier, cancellationToken); - - /// - public async Task CheckFhirServiceCapability(CancellationToken cancellationToken) - { - using FhirResponse response = await _fhirClient.ReadAsync("metadata", cancellationToken); - FHIRVersion version = response.Resource.FhirVersion ?? throw new InvalidFhirServerException(DicomCastCoreResource.FailedToValidateFhirVersion); - if (!_supportedFHIRVersions.Contains(version)) - { - throw new InvalidFhirServerException(DicomCastCoreResource.InvalidFhirServerVersion); - } - - foreach (RestComponent element in response.Resource.Rest) - { - foreach (SystemInteractionComponent interaction in element.Interaction) - { - if (interaction.Code == SystemRestfulInteraction.Transaction) - { - return; - } - } - } - - throw new InvalidFhirServerException(DicomCastCoreResource.FhirServerTransactionNotSupported); - } - - private async Task SearchByIdentifierAsync(Identifier identifier, CancellationToken cancellationToken) - where TResource : Resource, new() - { - EnsureArg.IsNotNull(identifier, nameof(identifier)); - - return (await SearchByQueryParameterAsync(identifier.ToSearchQueryParameter(), 1, cancellationToken)).FirstOrDefault(); - } - - private Task> SearchByIdentifierMultipleAsync(Identifier identifier, CancellationToken cancellationToken) - where TResource : Resource, new() - { - EnsureArg.IsNotNull(identifier, nameof(identifier)); - - return SearchByQueryParameterAsync(identifier.ToSearchQueryParameter(), null, cancellationToken); - } - - private async Task> SearchByQueryParameterAsync(string queryParameter, int? maxCount, CancellationToken cancellationToken) - where TResource : Resource, new() - { - EnsureArg.IsNotNullOrEmpty(queryParameter, nameof(queryParameter)); - - string fhirTypeName = ModelInfo.GetFhirTypeNameForType(typeof(TResource)); - if (!Enum.TryParse(fhirTypeName, out ResourceType resourceType)) - { - Debug.Assert(false, "Resource type could not be parsed from TResource"); - } - - Bundle bundle = await _fhirClient.SearchAsync( - resourceType, - queryParameter, - count: null, - cancellationToken); - - int matchCount = 0; - var results = new List(); - - while (bundle != null) - { - matchCount += bundle.Entry.Count; - - if (matchCount > maxCount) - { - // Multiple matches. - throw new MultipleMatchingResourcesException(typeof(TResource).Name); - } - - results.AddRange(bundle.Entry.Select(x => (TResource)x.Resource)); - - if (bundle.NextLink != null) - { - bundle = await _fhirClient.SearchAsync(bundle.NextLink.ToString(), cancellationToken); - } - else - { - break; - } - } - - // Validate to make sure the resource is valid. - foreach (TResource result in results) - { - _fhirResourceValidator.Validate(result); - } - - return results; - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Fhir/FhirTransactionExecutor.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Fhir/FhirTransactionExecutor.cs deleted file mode 100644 index 0981191332..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Fhir/FhirTransactionExecutor.cs +++ /dev/null @@ -1,123 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Globalization; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Hl7.Fhir.Model; -using Microsoft.Health.DicomCast.Core.Exceptions; -using Microsoft.Health.Fhir.Client; - -namespace Microsoft.Health.DicomCast.Core.Features.Fhir; - -/// -/// Provides functionality to execute a transaction in a FHIR server. -/// -public class FhirTransactionExecutor : IFhirTransactionExecutor -{ - private readonly IFhirClient _fhirClient; - - public FhirTransactionExecutor(IFhirClient fhirClient) - { - EnsureArg.IsNotNull(fhirClient, nameof(fhirClient)); - - _fhirClient = fhirClient; - } - - /// - public async Task ExecuteTransactionAsync(Bundle bundle, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(bundle, nameof(bundle)); - - Bundle responseBundle; - - try - { - responseBundle = await _fhirClient.PostBundleAsync(bundle, cancellationToken: cancellationToken); - } - catch (FhirClientException ex) - { - if (ex.StatusCode == HttpStatusCode.PreconditionFailed) - { - // The request failed because the resource was updated by some external process. - throw new ResourceConflictException(); - } - else if (ex.StatusCode == HttpStatusCode.TooManyRequests) - { - // The request failed because the server was too busy. - throw new ServerTooBusyException(); - } - - throw new TransactionFailedException(ex.OperationOutcome, ex); - } - - if (responseBundle == null) - { - throw new InvalidFhirResponseException(DicomCastCoreResource.MissingResponseBundle); - } - - if (responseBundle.Entry?.Count != bundle.Entry.Count) - { - throw new InvalidFhirResponseException(DicomCastCoreResource.MismatchBundleEntryCount); - } - - for (int index = 0; index < responseBundle.Entry.Count; index++) - { - Bundle.EntryComponent entry = responseBundle.Entry[index]; - - HttpStatusCode statusCode = ValidateBundleEntryAndGetStatusCode(entry, index); - - // Cache the parsed status code. - entry.Response.AddAnnotation(statusCode); - } - - return responseBundle; - } - - private static HttpStatusCode ValidateBundleEntryAndGetStatusCode(Bundle.EntryComponent entry, int entryIndex) - { - if (entry == null) - { - throw new InvalidFhirResponseException( - string.Format(CultureInfo.InvariantCulture, DicomCastCoreResource.MissingBundleEntry, entryIndex)); - } - - if (entry.Response == null) - { - throw new InvalidFhirResponseException( - string.Format(CultureInfo.InvariantCulture, DicomCastCoreResource.MissingBundleEntryResponse, entryIndex)); - } - - if (entry.Response.Status == null) - { - throw new InvalidFhirResponseException( - string.Format(CultureInfo.InvariantCulture, DicomCastCoreResource.MissingBundleEntryResponseStatus, entryIndex)); - } - - ReadOnlySpan statusSpan = entry.Response.Status.AsSpan(); - - // Based on the spec (http://hl7.org/fhir/R4/bundle-definitions.html#Bundle.entry.response.status), - // the status should be starting with 3 digit HTTP code and may contain the HTTP description associated - // with the status code. - if (statusSpan.Length < 3 || - (statusSpan.Length > 3 && statusSpan[3] != ' ') || - !Enum.TryParse(statusSpan.Slice(0, 3).ToString(), out HttpStatusCode parsedStatusCode)) - { - throw new InvalidFhirResponseException( - string.Format(CultureInfo.InvariantCulture, DicomCastCoreResource.InvalidBundleEntryResponseStatus, entry.Response.Status, entryIndex)); - } - - if ((int)parsedStatusCode < 200 || (int)parsedStatusCode >= 300) - { - throw new InvalidFhirResponseException( - string.Format(CultureInfo.InvariantCulture, DicomCastCoreResource.MismatchTransactionStatusCode, entry.Response.Status, entryIndex)); - } - - return parsedStatusCode; - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Fhir/IFhirResourceValidator.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Fhir/IFhirResourceValidator.cs deleted file mode 100644 index b1b5b0eca6..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Fhir/IFhirResourceValidator.cs +++ /dev/null @@ -1,20 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Hl7.Fhir.Model; - -namespace Microsoft.Health.DicomCast.Core.Features.Fhir; - -/// -/// Provides functionality to validate a resource. -/// -public interface IFhirResourceValidator -{ - /// - /// Validates the . - /// - /// The resource to be validated. - void Validate(Resource resource); -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Fhir/IFhirService.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Fhir/IFhirService.cs deleted file mode 100644 index d904ecd93c..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Fhir/IFhirService.cs +++ /dev/null @@ -1,56 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Hl7.Fhir.Model; -using Task = System.Threading.Tasks.Task; - -namespace Microsoft.Health.DicomCast.Core.Features.Fhir; - -/// -/// Provides functionalities to communicate with FHIR server. -/// -public interface IFhirService -{ - /// - /// Asynchronously retrieves an resource from FHIR server matching the . - /// - /// The identifier of the patient. - /// The cancellation token. - /// A task representing the retrieving operation. - Task RetrievePatientAsync(Identifier identifier, CancellationToken cancellationToken = default); - - /// - /// Asynchronously retrieves an resource from FHIR server matching the . - /// - /// The identifier of the study. - /// The cancellation token. - /// A task representing the retrieving operation. - Task RetrieveImagingStudyAsync(Identifier identifier, CancellationToken cancellationToken = default); - - /// - /// Asynchronously retrieves an resource from FHIR server matching the . - /// - /// The queryparameter for endPoint. - /// The cancellation token. - /// A task representing the retrieving operation. - Task RetrieveEndpointAsync(string queryParameter, CancellationToken cancellationToken = default); - - /// - /// Asynchronously retrieves multiple resources from FHIR server matching the . - /// - /// The identifier of the study. - /// The cancellation token. - /// A task representing the retrieving operation. - Task> RetrieveObservationsAsync(Identifier identifier, CancellationToken cancellationToken = default); - - /// - /// Validates that FHIR server is right version and supports transactions. - /// - /// The cancellation token. - Task CheckFhirServiceCapability(CancellationToken cancellationToken = default); -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Fhir/IFhirTransactionExecutor.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Fhir/IFhirTransactionExecutor.cs deleted file mode 100644 index b2b8e09f69..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Fhir/IFhirTransactionExecutor.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using Hl7.Fhir.Model; - -namespace Microsoft.Health.DicomCast.Core.Features.Fhir; - -/// -/// Provides functionality to execute a transaction in a FHIR server. -/// -public interface IFhirTransactionExecutor -{ - /// - /// Asynchronously executes a FHIR transaction. - /// - /// The transaction to execute.. - /// The cancellation token/ - /// A task representing the processing operation. - Task ExecuteTransactionAsync(Bundle bundle, CancellationToken cancellationToken); -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Fhir/IdentifierUtility.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Fhir/IdentifierUtility.cs deleted file mode 100644 index eb75f6b89d..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Fhir/IdentifierUtility.cs +++ /dev/null @@ -1,32 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using Hl7.Fhir.Model; - -namespace Microsoft.Health.DicomCast.Core.Features.Fhir; - -/// -/// Utility for creating an identifier for . -/// -public static class IdentifierUtility -{ - private const string DicomIdentifierSystem = "urn:dicom:uid"; - - /// - /// Creates an that represents the study specified by . - /// - /// - /// The identifier is generated based on the rules specified by https://www.hl7.org/fhir/imagingstudy.html#notes. - /// - /// The study instance UID. - /// The that represents this study. - public static Identifier CreateIdentifier(string studyInstanceUid) - { - EnsureArg.IsNotNullOrWhiteSpace(studyInstanceUid, nameof(studyInstanceUid)); - - return new Identifier(DicomIdentifierSystem, $"urn:oid:{studyInstanceUid}"); - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Fhir/InvalidFhirResponseException.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Fhir/InvalidFhirResponseException.cs deleted file mode 100644 index 13b9a3ed9b..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Fhir/InvalidFhirResponseException.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.DicomCast.Core.Features.Fhir; - -/// -/// Exception thrown when the FHIR response is invalid. -/// -public class InvalidFhirResponseException : FhirNonRetryableException -{ - public InvalidFhirResponseException(string message) - : base(message) - { - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Fhir/InvalidFhirServerException.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Fhir/InvalidFhirServerException.cs deleted file mode 100644 index cf0fa8abe0..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Fhir/InvalidFhirServerException.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.DicomCast.Core.Features.Fhir; - -/// -/// Exception thrown when the FHIR server configuration is invalid. -/// -public class InvalidFhirServerException : FhirNonRetryableException -{ - public InvalidFhirServerException(string message) - : base(message) - { - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Fhir/MultipleMatchingResourcesException.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Fhir/MultipleMatchingResourcesException.cs deleted file mode 100644 index 95b7312afd..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Fhir/MultipleMatchingResourcesException.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Globalization; -using EnsureThat; - -namespace Microsoft.Health.DicomCast.Core.Features.Fhir; - -/// -/// Exception thrown when multiple resources matching the criteria. -/// -public class MultipleMatchingResourcesException : FhirNonRetryableException -{ - public MultipleMatchingResourcesException(string resourceType) - : base(FormatMessage(resourceType)) - { - ResourceType = resourceType; - } - - public string ResourceType { get; } - - private static string FormatMessage(string resourceType) - { - EnsureArg.IsNotNullOrWhiteSpace(resourceType, nameof(resourceType)); - - return string.Format(CultureInfo.CurrentCulture, DicomCastCoreResource.MultipleMatchingResourcesFound, resourceType); - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Fhir/ResourceConflictException.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Fhir/ResourceConflictException.cs deleted file mode 100644 index 9c92d44423..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Fhir/ResourceConflictException.cs +++ /dev/null @@ -1,18 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Microsoft.Health.DicomCast.Core.Exceptions; - -namespace Microsoft.Health.DicomCast.Core.Features.Fhir; - -/// -/// Exception thrown when resource cannot be created or updated because the resource has been updated. -/// -public class ResourceConflictException : RetryableException -{ - public ResourceConflictException() - { - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Fhir/TransactionFailedException.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Fhir/TransactionFailedException.cs deleted file mode 100644 index 3a26825374..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Fhir/TransactionFailedException.cs +++ /dev/null @@ -1,23 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using Hl7.Fhir.Model; - -namespace Microsoft.Health.DicomCast.Core.Features.Fhir; - -/// -/// Exception thrown when the transaction fails. -/// -public class TransactionFailedException : FhirNonRetryableException -{ - public TransactionFailedException(OperationOutcome operationOutcome, Exception innerException) - : base(DicomCastCoreResource.TransactionFailed, innerException) - { - OperationOutcome = operationOutcome; - } - - public OperationOutcome OperationOutcome { get; } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/State/ISyncStateService.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/State/ISyncStateService.cs deleted file mode 100644 index bc27964b67..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/State/ISyncStateService.cs +++ /dev/null @@ -1,36 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.Health.DicomCast.Core.Features.State; - -/// -/// Service that supports CRUD on sync status -/// Should be operated by a single thread. -/// -public interface ISyncStateService -{ - /// - /// Get the current sync state. - /// - /// Cancellation token. - /// SyncState object with the details on current. - Task GetSyncStateAsync(CancellationToken cancellationToken = default); - - /// - /// Update the sync state after new dicom events have been processed successfully. - /// - /// Sync state represeting new processed state. - /// Cancellation token. - Task UpdateSyncStateAsync(SyncState newSyncState, CancellationToken cancellationToken = default); - - /// - /// Reset the sync state to process the dicom events from the begining. - /// - /// Cancellation token. - Task ResetSyncStateAsync(CancellationToken cancellationToken = default); -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/State/ISyncStateStore.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/State/ISyncStateStore.cs deleted file mode 100644 index 8cdb51f70a..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/State/ISyncStateStore.cs +++ /dev/null @@ -1,29 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.Health.DicomCast.Core.Features.State; - -/// -/// Read and persistent SyncState in a data store. -/// -public interface ISyncStateStore -{ - /// - /// Read from the data store. - /// - /// Cancellation token. - /// representing last successful sync./> - Task ReadAsync(CancellationToken cancellationToken = default); - - /// - /// Persist with updates from the latest successfull sync. - /// - /// State representing the last successful sync. - /// Cancellation token. - Task UpdateAsync(SyncState state, CancellationToken cancellationToken = default); -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/State/SyncState.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/State/SyncState.cs deleted file mode 100644 index 01899e749d..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/State/SyncState.cs +++ /dev/null @@ -1,39 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; - -namespace Microsoft.Health.DicomCast.Core.Features.State; - -/// -/// Current state of sync -/// -public class SyncState -{ - public SyncState(long syncedSequence, DateTimeOffset syncedDate) - { - SyncedSequence = syncedSequence; - SyncedDate = syncedDate; - } - - /// - /// Sequence number of the processed dicom event. - /// - public long SyncedSequence { get; } - - /// - /// Server time when the last dicom event was processed. - /// - public DateTimeOffset SyncedDate { get; } - - /// - /// Creates the model with initial state before the sync starts - /// - /// SyncState - public static SyncState CreateInitialSyncState() - { - return new SyncState(0, DateTime.MinValue); - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/State/SyncStateService.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/State/SyncStateService.cs deleted file mode 100644 index b606d6072d..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/State/SyncStateService.cs +++ /dev/null @@ -1,43 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; - -namespace Microsoft.Health.DicomCast.Core.Features.State; - -/// -public class SyncStateService : ISyncStateService -{ - private readonly ISyncStateStore _store; - - public SyncStateService(ISyncStateStore store) - { - EnsureArg.IsNotNull(store); - - _store = store; - } - - /// - public async Task GetSyncStateAsync(CancellationToken cancellationToken) - { - return await _store.ReadAsync(cancellationToken); - } - - /// - public async Task UpdateSyncStateAsync(SyncState newSyncState, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(newSyncState); - - await _store.UpdateAsync(newSyncState, cancellationToken); - } - - /// - public async Task ResetSyncStateAsync(CancellationToken cancellationToken) - { - await _store.UpdateAsync(SyncState.CreateInitialSyncState(), cancellationToken); - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/ChangeFeedProcessor.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/ChangeFeedProcessor.cs deleted file mode 100644 index 9894340387..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/ChangeFeedProcessor.cs +++ /dev/null @@ -1,202 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Client.Models; -using Microsoft.Health.DicomCast.Core.Configurations; -using Microsoft.Health.DicomCast.Core.Exceptions; -using Microsoft.Health.DicomCast.Core.Features.DicomWeb.Service; -using Microsoft.Health.DicomCast.Core.Features.ExceptionStorage; -using Microsoft.Health.DicomCast.Core.Features.Fhir; -using Microsoft.Health.DicomCast.Core.Features.State; -using Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; -using Polly.Timeout; -using Task = System.Threading.Tasks.Task; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker; - -/// -/// Provides functionality to process the change feed. -/// -public class ChangeFeedProcessor : IChangeFeedProcessor -{ - internal const int DefaultLimit = 10; - private static readonly Func LogProcessingDelegate = LoggerMessage.DefineScope("Processing change feed."); - - private readonly IChangeFeedRetrieveService _changeFeedRetrieveService; - private readonly IFhirTransactionPipeline _fhirTransactionPipeline; - private readonly ISyncStateService _syncStateService; - private readonly IExceptionStore _exceptionStore; - private readonly TimeProvider _timeProvider; - private readonly DicomCastConfiguration _configuration; - private readonly ILogger _logger; - - public ChangeFeedProcessor( - IChangeFeedRetrieveService changeFeedRetrieveService, - IFhirTransactionPipeline fhirTransactionPipeline, - ISyncStateService syncStateService, - IExceptionStore exceptionStore, - IOptions dicomCastConfiguration, - ILogger logger) - : this(changeFeedRetrieveService, fhirTransactionPipeline, syncStateService, exceptionStore, TimeProvider.System, dicomCastConfiguration, logger) - { } - - internal ChangeFeedProcessor( - IChangeFeedRetrieveService changeFeedRetrieveService, - IFhirTransactionPipeline fhirTransactionPipeline, - ISyncStateService syncStateService, - IExceptionStore exceptionStore, - TimeProvider timeProvider, - IOptions dicomCastConfiguration, - ILogger logger) - { - _changeFeedRetrieveService = EnsureArg.IsNotNull(changeFeedRetrieveService, nameof(changeFeedRetrieveService)); - _fhirTransactionPipeline = EnsureArg.IsNotNull(fhirTransactionPipeline, nameof(fhirTransactionPipeline)); - _syncStateService = EnsureArg.IsNotNull(syncStateService, nameof(syncStateService)); - _exceptionStore = EnsureArg.IsNotNull(exceptionStore, nameof(exceptionStore)); - _timeProvider = EnsureArg.IsNotNull(timeProvider, nameof(timeProvider)); - _configuration = EnsureArg.IsNotNull(dicomCastConfiguration?.Value, nameof(dicomCastConfiguration)); - _logger = EnsureArg.IsNotNull(logger, nameof(logger)); - } - - /// - public async Task ProcessAsync(TimeSpan pollIntervalDuringCatchup, CancellationToken cancellationToken) - { - using (LogProcessingDelegate(_logger)) - { - SyncState state = await _syncStateService.GetSyncStateAsync(cancellationToken); - - while (true) - { - // Retrieve the change feed for any changes after checking the sequence number of the latest event - long latest = await _changeFeedRetrieveService.RetrieveLatestSequenceAsync(cancellationToken); - IReadOnlyList changeFeedEntries = await GetChangeFeedEntries(state, _configuration.Features.IgnoreJsonParsingErrors, cancellationToken); - - // If there are no events because nothing available, then skip processing for now - // Note that there may be more events to read for API version v1 even if the Count < limit - if (changeFeedEntries.Count == 0 && latest == state.SyncedSequence) - { - _logger.LogInformation("No new DICOM events to process"); - return; - } - - // Otherwise, process any new entries and increment the sequence - long maxSequence = changeFeedEntries.Count > 0 ? changeFeedEntries[^1].Sequence : state.SyncedSequence + DefaultLimit; - await ProcessChangeFeedEntriesAsync(changeFeedEntries, cancellationToken); - - var newSyncState = new SyncState(maxSequence, _timeProvider.GetUtcNow()); - await _syncStateService.UpdateSyncStateAsync(newSyncState, cancellationToken); - _logger.LogInformation("Processed DICOM events sequenced [{SequenceId}, {MaxSequence}]", state.SyncedSequence + 1, maxSequence); - state = newSyncState; - - await Task.Delay(pollIntervalDuringCatchup, cancellationToken); - } - } - } - - private async Task> GetChangeFeedEntries(SyncState state, bool ignoreJsonParsingErrors, CancellationToken cancellationToken) - { - try - { - return await _changeFeedRetrieveService.RetrieveChangeFeedAsync( - state.SyncedSequence, - DefaultLimit, - cancellationToken); - } - catch (JsonException) - { - if (!ignoreJsonParsingErrors) - { - throw; - } - - return await GetChangeFeedEntriesOneByOne(state, cancellationToken); - } - } - - private async Task> GetChangeFeedEntriesOneByOne(SyncState state, CancellationToken cancellationToken) - { - long start = state.SyncedSequence; - List changeFeedEntries = []; - - while (start < state.SyncedSequence + DefaultLimit) - { - try - { - var changeFeedEntry = await _changeFeedRetrieveService.RetrieveChangeFeedAsync(start, 1, cancellationToken); - - if (changeFeedEntry == null || changeFeedEntry.Count == 0) - { - return changeFeedEntries.AsReadOnly(); - } - - start++; - changeFeedEntries.Add(changeFeedEntry[0]); - } - catch (JsonException ex) - { - // ignore items that failed to parse - _logger.LogError(ex, "Changefeed entry with SequenceId {SequenceId} failed to be parsed by the DicomWebClient", start); - start++; - } - } - - return changeFeedEntries.AsReadOnly(); - } - - private async Task ProcessChangeFeedEntriesAsync(IEnumerable changeFeedEntries, CancellationToken cancellationToken) - { - // Process each change feed as a FHIR transaction. - foreach (ChangeFeedEntry changeFeedEntry in changeFeedEntries) - { - try - { - if (!(changeFeedEntry.Action == ChangeFeedAction.Create && changeFeedEntry.State == ChangeFeedState.Deleted)) - { - await _fhirTransactionPipeline.ProcessAsync(changeFeedEntry, cancellationToken); - _logger.LogInformation("Successfully processed DICOM event with SequenceID: {SequenceId}", changeFeedEntry.Sequence); - } - else - { - _logger.LogInformation("Skip DICOM event with SequenceId {SequenceId} due to deletion before processing creation.", changeFeedEntry.Sequence); - } - } - catch (Exception ex) when (ex is FhirNonRetryableException or DicomTagException or TimeoutRejectedException) - { - string studyInstanceUid = changeFeedEntry.StudyInstanceUid; - string seriesInstanceUid = changeFeedEntry.SeriesInstanceUid; - string sopInstanceUid = changeFeedEntry.SopInstanceUid; - long changeFeedSequence = changeFeedEntry.Sequence; - - ErrorType errorType = ErrorType.FhirError; - - if (ex is DicomTagException) - { - errorType = ErrorType.DicomError; - } - else if (ex is TimeoutRejectedException) - { - errorType = ErrorType.TransientFailure; - } - - await _exceptionStore.WriteExceptionAsync(changeFeedEntry, ex, errorType, cancellationToken); - - _logger.LogError( - "Failed to process DICOM event with SequenceID: {SequenceId}, StudyUid: {StudyInstanceUid}, SeriesUid: {SeriesInstanceUid}, instanceUid: {SopInstanceUid} and will not be retried further. Continuing to next event.", - changeFeedSequence, - studyInstanceUid, - seriesInstanceUid, - sopInstanceUid); - } - } - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/Constants.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/Constants.cs deleted file mode 100644 index 1469120d9f..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/Constants.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.DicomCast.Core.Features.Worker; -public static class Constants -{ - // Represents different metrics the dicomcastworker will emit. - public const string CastToFhirForbidden = "Cast-To-Fhir-Forbidden"; - public const string DicomToCastForbidden = "Dicom-To-Cast-Forbidden"; - public const string CastMIUnavailable = "Cast-Mi-Unavailable"; - public const string CastingFailedForOtherReasons = "Casting-Failed"; -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/DicomCastMeter.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/DicomCastMeter.cs deleted file mode 100644 index f987267ddb..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/DicomCastMeter.cs +++ /dev/null @@ -1,34 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Diagnostics.Metrics; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker; - -public sealed class DicomCastMeter : IDisposable -{ - private readonly Meter _meter; - - public DicomCastMeter() - { - _meter = new Meter("Microsoft.Health.DicomCast", "1.0"); - CastToFhirForbidden = _meter.CreateCounter(nameof(CastToFhirForbidden), description: "DicomCast failed due to a 403 (Forbidden) response from the FHIR server."); - DicomToCastForbidden = _meter.CreateCounter(nameof(DicomToCastForbidden), description: "Dicom casting forbidden"); - CastMIUnavailable = _meter.CreateCounter(nameof(CastMIUnavailable), description: "Managed Identity unavailable"); - CastingFailedForOtherReasons = _meter.CreateCounter(nameof(CastingFailedForOtherReasons), description: "Casting failed due to other reasons"); - } - - public Counter CastToFhirForbidden { get; } - - public Counter DicomToCastForbidden { get; } - - public Counter CastMIUnavailable { get; } - - public Counter CastingFailedForOtherReasons { get; } - - public void Dispose() - => _meter.Dispose(); -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/DicomCastWorker.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/DicomCastWorker.cs deleted file mode 100644 index cc59b92996..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/DicomCastWorker.cs +++ /dev/null @@ -1,135 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Diagnostics.CodeAnalysis; -using System.Net; -using System.Threading; -using Azure.Identity; -using EnsureThat; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Client; -using Microsoft.Health.DicomCast.Core.Configurations; -using Microsoft.Health.DicomCast.Core.Features.Fhir; -using Microsoft.Health.Fhir.Client; -using Task = System.Threading.Tasks.Task; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker; - -/// -/// The worker for DicomCast. -/// -public class DicomCastWorker : IDicomCastWorker -{ - private static readonly Action LogWorkerStartingDelegate = - LoggerMessage.Define( - LogLevel.Information, - default, - $"{typeof(DicomCastWorker)} is starting."); - - private static readonly Action LogWorkerCancelRequestedDelegate = - LoggerMessage.Define( - LogLevel.Information, - default, - $"{typeof(DicomCastWorker)} is requested to be stopped."); - - private static readonly Action LogWorkerExitingDelegate = - LoggerMessage.Define( - LogLevel.Information, - default, - $"{typeof(DicomCastWorker)} is exiting."); - - private static readonly Action LogUnhandledExceptionDelegate = - LoggerMessage.Define( - LogLevel.Critical, - default, - "Unhandled exception."); - - private readonly DicomCastWorkerConfiguration _dicomCastWorkerConfiguration; - private readonly IChangeFeedProcessor _changeFeedProcessor; - private readonly ILogger _logger; - private readonly IHostApplicationLifetime _hostApplicationLifetime; - private readonly IFhirService _fhirService; - private readonly DicomCastMeter _dicomCastMeter; - - public DicomCastWorker( - IOptions dicomCastWorkerConfiguration, - IChangeFeedProcessor changeFeedProcessor, - ILogger logger, - IHostApplicationLifetime hostApplicationLifetime, - IFhirService fhirService, - DicomCastMeter dicomCastMeter) - { - EnsureArg.IsNotNull(dicomCastWorkerConfiguration?.Value, nameof(dicomCastWorkerConfiguration)); - EnsureArg.IsNotNull(changeFeedProcessor, nameof(changeFeedProcessor)); - EnsureArg.IsNotNull(logger, nameof(logger)); - EnsureArg.IsNotNull(hostApplicationLifetime, nameof(hostApplicationLifetime)); - EnsureArg.IsNotNull(fhirService, nameof(fhirService)); - EnsureArg.IsNotNull(dicomCastMeter, nameof(dicomCastMeter)); - - _dicomCastWorkerConfiguration = dicomCastWorkerConfiguration.Value; - _changeFeedProcessor = changeFeedProcessor; - _logger = logger; - _hostApplicationLifetime = hostApplicationLifetime; - _fhirService = fhirService; - _dicomCastMeter = dicomCastMeter; - } - - /// - [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "App shuts down on any error.")] - public async Task ExecuteAsync(CancellationToken cancellationToken) - { - try - { - await _fhirService.CheckFhirServiceCapability(cancellationToken); - LogWorkerStartingDelegate(_logger, null); - - while (!cancellationToken.IsCancellationRequested) - { - try - { - await _changeFeedProcessor.ProcessAsync(_dicomCastWorkerConfiguration.PollIntervalDuringCatchup, cancellationToken); - - await Task.Delay(_dicomCastWorkerConfiguration.PollInterval, cancellationToken); - } - catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) - { - // Cancel requested. - LogWorkerCancelRequestedDelegate(_logger, null); - break; - } - } - - LogWorkerExitingDelegate(_logger, null); - } - catch (Exception ex) - { - LogUnhandledExceptionDelegate(_logger, ex); - - if (ex is FhirClientException fce && fce.StatusCode == HttpStatusCode.Forbidden) - { - _dicomCastMeter.CastToFhirForbidden.Add(1); - } - else if (ex is DicomWebException dwe && dwe.StatusCode == HttpStatusCode.Forbidden) - { - _dicomCastMeter.DicomToCastForbidden.Add(1); - } - else if (ex is CredentialUnavailableException) - { - _dicomCastMeter.CastMIUnavailable.Add(1); - } - else - { - _dicomCastMeter.CastingFailedForOtherReasons.Add(1); - } - - // Any exception in ExecuteAsync will not shutdown application, call hostApplicationLifetime.StopApplication() to force shutdown. - // Please refer to .net core issue on github for more details: "Exceptions in BackgroundService ExecuteAsync are (sometimes) hidden" https://github.com/dotnet/extensions/issues/2363 - _hostApplicationLifetime.StopApplication(); - } - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/EndpointPipelineStep.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/EndpointPipelineStep.cs deleted file mode 100644 index 97c81b5b70..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/EndpointPipelineStep.cs +++ /dev/null @@ -1,113 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Threading; -using EnsureThat; -using Hl7.Fhir.Model; -using Hl7.Fhir.Utility; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Health.DicomCast.Core.Configurations; -using Microsoft.Health.DicomCast.Core.Extensions; -using Microsoft.Health.DicomCast.Core.Features.Fhir; -using Task = System.Threading.Tasks.Task; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -public class EndpointPipelineStep : FhirTransactionPipelineStepBase -{ - private readonly IFhirService _fhirService; - private readonly string _dicomWebEndpoint; - - public EndpointPipelineStep( - IOptions dicomWebConfiguration, - IFhirService fhirService, - ILogger logger) - : base(logger) - { - EnsureArg.IsNotNull(dicomWebConfiguration?.Value, nameof(dicomWebConfiguration)); - EnsureArg.IsNotNull(fhirService, nameof(fhirService)); - - _fhirService = fhirService; - _dicomWebEndpoint = dicomWebConfiguration.Value.Endpoint.ToString(); - } - - protected override async Task PrepareRequestImplementationAsync(FhirTransactionContext context, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(context, nameof(context)); - - string endpointName = $"{FhirTransactionConstants.EndpointName} {_dicomWebEndpoint}"; - - string queryParameter = $"name={endpointName}&connection-type={FhirTransactionConstants.EndpointConnectionTypeSystem}|{FhirTransactionConstants.EndpointConnectionTypeCode}"; - - Endpoint endpoint = await _fhirService.RetrieveEndpointAsync(queryParameter, cancellationToken); - - FhirTransactionRequestMode requestMode = FhirTransactionRequestMode.None; - - if (endpoint == null) - { - endpoint = new Endpoint() - { - Name = endpointName, - Status = Endpoint.EndpointStatus.Active, - ConnectionType = new Coding() - { - System = FhirTransactionConstants.EndpointConnectionTypeSystem, - Code = FhirTransactionConstants.EndpointConnectionTypeCode, - }, - Address = _dicomWebEndpoint, - PayloadType = new List - { - new CodeableConcept(string.Empty, string.Empty, FhirTransactionConstants.EndpointPayloadTypeText), - }, - PayloadMimeType = new string[] - { - FhirTransactionConstants.DicomMimeType, - }, - }; - - requestMode = FhirTransactionRequestMode.Create; - } - else - { - // Make sure the address matches. - if (!string.Equals(endpoint.Address, _dicomWebEndpoint, StringComparison.Ordinal)) - { - // We have found an endpoint with matching name and connection-type but the address does not match. - throw new FhirResourceValidationException(DicomCastCoreResource.MismatchEndpointAddress); - } - } - - Bundle.RequestComponent request = requestMode switch - { - FhirTransactionRequestMode.Create => new Bundle.RequestComponent() - { - Method = Bundle.HTTPVerb.POST, - IfNoneExist = queryParameter, - Url = ResourceType.Endpoint.GetLiteral(), - }, - _ => null, - }; - - IResourceId resourceId = requestMode switch - { - FhirTransactionRequestMode.Create => new ClientResourceId(), - _ => endpoint.ToServerResourceId(), - }; - - context.Request.Endpoint = new FhirTransactionRequestEntry( - requestMode, - request, - resourceId, - endpoint); - } - - protected override void ProcessResponseImplementation(FhirTransactionContext context) - { - // No action needed. - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/FhirTransactionConstants.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/FhirTransactionConstants.cs deleted file mode 100644 index 1d402de7fc..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/FhirTransactionConstants.cs +++ /dev/null @@ -1,23 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -public static class FhirTransactionConstants -{ - public const string EndpointConnectionTypeSystem = "http://terminology.hl7.org/CodeSystem/endpoint-connection-type"; - public const string EndpointConnectionTypeCode = "dicom-wado-rs"; - public const string EndpointName = "DICOM WADO-RS endpoint"; - public const string EndpointPayloadTypeText = "DICOM WADO-RS"; - public const string DicomMimeType = "application/dicom"; - - public const string ModalityInSystem = "DCM"; - - public const string AccessionNumberTypeSystem = "http://terminology.hl7.org/CodeSystem/v2-0203"; - public const string AccessionNumberTypeCode = "ACSN"; - - // Refer to https://docs.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings#zzzSpecifier for how to convert in c# - public const string UtcTimezoneOffsetFormat = "zzz"; -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/FhirTransactionContext.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/FhirTransactionContext.cs deleted file mode 100644 index e1676ce617..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/FhirTransactionContext.cs +++ /dev/null @@ -1,46 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using EnsureThat; -using Microsoft.Health.Dicom.Client.Models; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -/// -/// Provides the context of a transaction. -/// -public class FhirTransactionContext : IFhirTransactionContext -{ - public FhirTransactionContext(ChangeFeedEntry changeFeedEntry) - { - EnsureArg.IsNotNull(changeFeedEntry, nameof(changeFeedEntry)); - - ChangeFeedEntry = changeFeedEntry; - - Request = new FhirTransactionRequest(); - Response = new FhirTransactionResponse(); - } - - /// - /// Gets the change feed used for this transaction. - /// - public ChangeFeedEntry ChangeFeedEntry { get; } - - /// - /// Gets the request. - /// - public FhirTransactionRequest Request { get; } - - /// - /// Gets the response. - /// - public FhirTransactionResponse Response { get; } - - /// - /// Gets or sets the utcDatetimeOffset for changefeedEntry dataset for this transaction. - /// - public TimeSpan UtcDateTimeOffset { get; set; } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/FhirTransactionPipeline.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/FhirTransactionPipeline.cs deleted file mode 100644 index 6c124cbb85..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/FhirTransactionPipeline.cs +++ /dev/null @@ -1,210 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Hl7.Fhir.Model; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Client.Models; -using Microsoft.Health.DicomCast.Core.Configurations; -using Microsoft.Health.DicomCast.Core.Exceptions; -using Microsoft.Health.DicomCast.Core.Features.ExceptionStorage; -using Microsoft.Health.DicomCast.Core.Features.Fhir; -using Polly; -using Polly.Timeout; -using Task = System.Threading.Tasks.Task; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -/// -/// Provides functionality to build and process response of a FHIR transaction. -/// -public class FhirTransactionPipeline : IFhirTransactionPipeline -{ - private readonly IEnumerable _fhirTransactionPipelines; - private readonly IFhirTransactionExecutor _fhirTransactionExecutor; - - private readonly IReadOnlyList _requestResponsePropertyAccessors; - private readonly IAsyncPolicy _retryPolicy; - private readonly IExceptionStore _exceptionStore; - private readonly AsyncTimeoutPolicy _timeoutPolicy; - private readonly ILogger _logger; - - public FhirTransactionPipeline( - IEnumerable fhirTransactionPipelines, - IFhirTransactionRequestResponsePropertyAccessors fhirTransactionRequestResponsePropertyAccessors, - IFhirTransactionExecutor fhirTransactionExecutor, - IExceptionStore exceptionStore, - IOptions retryConfiguration, - ILogger logger) - { - _fhirTransactionPipelines = EnsureArg.IsNotNull(fhirTransactionPipelines, nameof(fhirTransactionPipelines)); - _requestResponsePropertyAccessors = EnsureArg.IsNotNull(fhirTransactionRequestResponsePropertyAccessors?.PropertyAccessors, nameof(fhirTransactionRequestResponsePropertyAccessors)); - _fhirTransactionExecutor = EnsureArg.IsNotNull(fhirTransactionExecutor, nameof(fhirTransactionExecutor)); - _exceptionStore = EnsureArg.IsNotNull(exceptionStore, nameof(exceptionStore)); - EnsureArg.IsNotNull(retryConfiguration?.Value, nameof(retryConfiguration)); - _logger = EnsureArg.IsNotNull(logger, nameof(logger)); - - _timeoutPolicy = Policy.TimeoutAsync(retryConfiguration.Value.TotalRetryDuration); - _retryPolicy = Policy - .Handle() - .WaitAndRetryForeverAsync( - (retryAttempt, exception, context) => - { - return TimeSpan.FromSeconds(Math.Min(60, Math.Pow(2, retryAttempt))); - }, - (exception, retryCount, timeSpan, context) => - { - var changeFeedEntry = (ChangeFeedEntry)context[nameof(ChangeFeedEntry)]; - - return _exceptionStore.WriteRetryableExceptionAsync( - changeFeedEntry, - retryCount, - timeSpan, - exception); - }); - } - - /// - public async Task ProcessAsync(ChangeFeedEntry changeFeedEntry, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(changeFeedEntry, nameof(changeFeedEntry)); - - try - { - var context = new Context - { - { nameof(ChangeFeedEntry), changeFeedEntry }, - }; - - await _timeoutPolicy.WrapAsync(_retryPolicy).ExecuteAsync( - async (ctx, tkn) => - { - try - { - var entry = (ChangeFeedEntry)ctx[nameof(ChangeFeedEntry)]; - - // Create a context used throughout this process. - var fhirTransactionContext = new FhirTransactionContext(entry); - - // Prepare all required objects for the transaction. - foreach (IFhirTransactionPipelineStep pipeline in _fhirTransactionPipelines) - { - await pipeline.PrepareRequestAsync(fhirTransactionContext, tkn); - } - - // Check to see if any resource needs to be created/updated. - var bundle = new Bundle() - { - Type = Bundle.BundleType.Transaction, - }; - - var usedPropertyAccessors = new List<(FhirTransactionRequestResponsePropertyAccessor Accessor, int Count)>(_requestResponsePropertyAccessors.Count); - - foreach (FhirTransactionRequestResponsePropertyAccessor propertyAccessor in _requestResponsePropertyAccessors) - { - List requestEntries = propertyAccessor.RequestEntryGetter(fhirTransactionContext.Request)?.ToList(); - - if (requestEntries == null || requestEntries.Count == 0) - { - continue; - } - - int useCount = 0; - foreach (FhirTransactionRequestEntry requestEntry in requestEntries) - { - if (requestEntry == null || requestEntry.RequestMode == FhirTransactionRequestMode.None) - { - // No associated request, skip it. - continue; - } - - // There is a associated request, add to the list so it gets processed. - bundle.Entry.Add(CreateRequestBundleEntryComponent(requestEntry)); - useCount++; - } - - usedPropertyAccessors.Add((propertyAccessor, useCount)); - } - - if (bundle.Entry.Count == 0) - { - // Nothing to update. - return; - } - - // Execute the transaction. - Bundle responseBundle = await _fhirTransactionExecutor.ExecuteTransactionAsync(bundle, tkn); - - // Process the response. - int processedResponseItems = 0; - - foreach ((FhirTransactionRequestResponsePropertyAccessor accessor, int count) in - usedPropertyAccessors.Where(x => x.Count > 0)) - { - var responseEntries = new List(); - for (int j = 0; j < count; j++) - { - FhirTransactionResponseEntry responseEntry = CreateResponseEntry(responseBundle.Entry[processedResponseItems + j]); - responseEntries.Add(responseEntry); - } - - processedResponseItems += count; - accessor.ResponseEntrySetter(fhirTransactionContext.Response, responseEntries); - } - - // Execute any additional checks of the response. - foreach (IFhirTransactionPipelineStep pipeline in _fhirTransactionPipelines) - { - pipeline.ProcessResponse(fhirTransactionContext); - } - - _logger.LogInformation("Successfully processed the change feed entry."); - } - catch (TaskCanceledException ex) when (!tkn.IsCancellationRequested) - { - throw new RetryableException(ex); - } - catch (HttpRequestException ex) - { - throw new RetryableException(ex); - } - }, - context, - cancellationToken); - } - catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) - { - // Cancel requested - throw; - } - catch (Exception ex) - { - _logger.LogError(ex, "Encountered an exception while processing the change feed entry."); - throw; - } - - static Bundle.EntryComponent CreateRequestBundleEntryComponent(FhirTransactionRequestEntry requestEntry) - { - return new Bundle.EntryComponent() - { - FullUrl = requestEntry.ResourceId.ToString(), - Request = requestEntry.Request, - Resource = requestEntry.Resource, - }; - } - - static FhirTransactionResponseEntry CreateResponseEntry(Bundle.EntryComponent response) - { - return new FhirTransactionResponseEntry(response.Response, response.Resource); - } - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/FhirTransactionPipelineStepBase.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/FhirTransactionPipelineStepBase.cs deleted file mode 100644 index c4c7d565b9..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/FhirTransactionPipelineStepBase.cs +++ /dev/null @@ -1,80 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.Extensions.Logging; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -public abstract class FhirTransactionPipelineStepBase : IFhirTransactionPipelineStep -{ - private static readonly Func LogPreparingRequestDelegate = - LoggerMessage.DefineScope($"Executing {nameof(PrepareRequestAsync)} of {{PipelineStep}}."); - - private static readonly Func LogProcessingResponseDelegate = - LoggerMessage.DefineScope($"Executing {nameof(ProcessResponse)} of {{PipelineStep}}."); - - private static readonly Action LogExceptionDelegate = - LoggerMessage.Define( - LogLevel.Error, - default, - "Encountered an exception while processing."); - - private readonly ILogger _logger; - private readonly string _pipelineStepName; - - protected FhirTransactionPipelineStepBase( - ILogger logger) - { - EnsureArg.IsNotNull(logger, nameof(logger)); - - _logger = logger; - _pipelineStepName = GetType().Name; - } - - public async Task PrepareRequestAsync(FhirTransactionContext context, CancellationToken cancellationToken = default) - { - using (LogPreparingRequestDelegate(_logger, _pipelineStepName)) - { - try - { - await PrepareRequestImplementationAsync(context, cancellationToken); - } - catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) - { - // Cancel requested. - throw; - } - catch (Exception ex) - { - LogExceptionDelegate(_logger, ex); - throw; - } - } - } - - public void ProcessResponse(FhirTransactionContext context) - { - using (LogProcessingResponseDelegate(_logger, _pipelineStepName)) - { - try - { - ProcessResponseImplementation(context); - } - catch (Exception ex) - { - LogExceptionDelegate(_logger, ex); - throw; - } - } - } - - protected abstract Task PrepareRequestImplementationAsync(FhirTransactionContext context, CancellationToken cancellationToken = default); - - protected abstract void ProcessResponseImplementation(FhirTransactionContext context); -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/FhirTransactionRequest.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/FhirTransactionRequest.cs deleted file mode 100644 index db2e53a16b..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/FhirTransactionRequest.cs +++ /dev/null @@ -1,25 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -/// -/// Provides list of request that will be used to execute the FHIR transaction. -/// -public class FhirTransactionRequest : IFhirTransactionRequestResponse -{ - /// - public FhirTransactionRequestEntry Patient { get; set; } - - /// - public FhirTransactionRequestEntry Endpoint { get; set; } - - /// - public FhirTransactionRequestEntry ImagingStudy { get; set; } - - public IEnumerable Observation { get; set; } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/FhirTransactionRequestEntry.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/FhirTransactionRequestEntry.cs deleted file mode 100644 index 9fb9f9f6f7..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/FhirTransactionRequestEntry.cs +++ /dev/null @@ -1,49 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using Hl7.Fhir.Model; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -/// -/// Provides a FHIR transaction request detail. -/// -public class FhirTransactionRequestEntry -{ - public FhirTransactionRequestEntry( - FhirTransactionRequestMode requestMode, - Bundle.RequestComponent request, - IResourceId resourceId, - Resource resource) - { - EnsureArg.EnumIsDefined(requestMode, nameof(requestMode)); - - RequestMode = requestMode; - Request = request; - ResourceId = resourceId; - Resource = resource; - } - - /// - /// Gets the request mode. - /// - public FhirTransactionRequestMode RequestMode { get; } - - /// - /// Gets the request component. - /// - public Bundle.RequestComponent Request { get; } - - /// - /// Gets the request resource id. - /// - public IResourceId ResourceId { get; } - - /// - /// Gets the request resource. - /// - public Resource Resource { get; } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/FhirTransactionRequestMode.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/FhirTransactionRequestMode.cs deleted file mode 100644 index 2750d98201..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/FhirTransactionRequestMode.cs +++ /dev/null @@ -1,32 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -/// -/// Represents the request mode. -/// -public enum FhirTransactionRequestMode -{ - /// - /// The resource did not change; no request needed. - /// - None, - - /// - /// The resource needs to be created. - /// - Create, - - /// - /// The resource needs to be updated. - /// - Update, - - /// - /// The resource needs to be deleted. - /// - Delete, -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/FhirTransactionRequestResponsePropertyAccessor.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/FhirTransactionRequestResponsePropertyAccessor.cs deleted file mode 100644 index 7ee9022b72..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/FhirTransactionRequestResponsePropertyAccessor.cs +++ /dev/null @@ -1,84 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using EnsureThat; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -/// -/// Provides delegate to getting the and setting the for a given property. -/// -public struct FhirTransactionRequestResponsePropertyAccessor : IEquatable -{ - public FhirTransactionRequestResponsePropertyAccessor( - string propertyName, - Func> requestEntryGetter, - Action> responseEntrySetter) - { - EnsureArg.IsNotNullOrWhiteSpace(propertyName, nameof(propertyName)); - EnsureArg.IsNotNull(requestEntryGetter, nameof(requestEntryGetter)); - EnsureArg.IsNotNull(responseEntrySetter, nameof(responseEntrySetter)); - - PropertyName = propertyName; - RequestEntryGetter = requestEntryGetter; - ResponseEntrySetter = responseEntrySetter; - } - - /// - /// Gets the property name. - /// - public string PropertyName { get; } - - /// - /// Gets the property getter for . - /// - public Func> RequestEntryGetter { get; } - - /// - /// Gets the property setter for . - /// - public Action> ResponseEntrySetter { get; } - - public static bool operator ==(FhirTransactionRequestResponsePropertyAccessor left, FhirTransactionRequestResponsePropertyAccessor right) - { - return left.Equals(right); - } - - public static bool operator !=(FhirTransactionRequestResponsePropertyAccessor left, FhirTransactionRequestResponsePropertyAccessor right) - { - return !(left == right); - } - - public override bool Equals(object obj) - { - if (!(obj is FhirTransactionRequestResponsePropertyAccessor other)) - { - return false; - } - - return Equals(other); - } - - public override int GetHashCode() - { - return HashCode.Combine(PropertyName, RequestEntryGetter.Method, ResponseEntrySetter.Method); - } - - public bool Equals(FhirTransactionRequestResponsePropertyAccessor other) - { - if (GetType() != other.GetType()) - { - return false; - } - else - { - return string.Equals(PropertyName, other.PropertyName, StringComparison.Ordinal) && - Equals(RequestEntryGetter, other.RequestEntryGetter) && - Equals(ResponseEntrySetter, other.ResponseEntrySetter); - } - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/FhirTransactionRequestResponsePropertyAccessors.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/FhirTransactionRequestResponsePropertyAccessors.cs deleted file mode 100644 index 92927dadd8..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/FhirTransactionRequestResponsePropertyAccessors.cs +++ /dev/null @@ -1,88 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -/// -/// Provides property accessors for and . -/// -public class FhirTransactionRequestResponsePropertyAccessors : IFhirTransactionRequestResponsePropertyAccessors -{ - private readonly FhirTransactionRequestResponsePropertyAccessor[] _propertiesAccessors; - - public FhirTransactionRequestResponsePropertyAccessors() - { - // Get the list of properties from the interface. - PropertyInfo[] interfaceProperties = typeof(IFhirTransactionRequestResponse<>).GetProperties(BindingFlags.Public | BindingFlags.Instance); - - _propertiesAccessors = interfaceProperties.Select(interfacePropertyInfo => Create(interfacePropertyInfo)) - .OrderBy(propertyAccessor => propertyAccessor.PropertyName) - .ToArray(); - - static FhirTransactionRequestResponsePropertyAccessor Create(PropertyInfo interfacePropertyInfo) - { - Func> requestPropertyGetterDelegate = CreateGetterDelegate(interfacePropertyInfo); - - Action> responsePropertySetterDelegate = CreateSetterDelegate(interfacePropertyInfo); - - return new FhirTransactionRequestResponsePropertyAccessor(interfacePropertyInfo.Name, requestPropertyGetterDelegate, responsePropertySetterDelegate); - } - } - - private static Func> CreateGetterDelegate(PropertyInfo propertyInfo) - { - ParameterExpression parameterExpression = Expression.Parameter(typeof(FhirTransactionRequest)); - MemberExpression propertyExpression = Expression.Property(parameterExpression, propertyInfo.Name); - - Expression bodyExpression; - if (typeof(IEnumerable).IsAssignableFrom(propertyInfo.PropertyType)) - { - bodyExpression = propertyExpression; - } - else - { - bodyExpression = Expression.NewArrayInit(typeof(FhirTransactionRequestEntry), propertyExpression); - } - - return Expression.Lambda>>(bodyExpression, parameterExpression).Compile(); - } - - private static Action> CreateSetterDelegate(PropertyInfo propertyInfo) - { - ParameterExpression parameterExpression = Expression.Parameter(typeof(FhirTransactionResponse)); - ParameterExpression enumerableParameterExpression = Expression.Parameter(typeof(IEnumerable)); - MemberExpression propertyExpression = Expression.Property(parameterExpression, propertyInfo.Name); - - Expression bodyExpression; - if (typeof(IEnumerable).IsAssignableFrom(propertyInfo.PropertyType)) - { - bodyExpression = Expression.Assign(propertyExpression, enumerableParameterExpression); - } - else - { - MethodInfo singleMethod = typeof(Enumerable) - .GetMethods() - .Single( - x => x.Name == nameof(Enumerable.Single) && - x.IsGenericMethod && - x.GetParameters().Length == 1) - .MakeGenericMethod(typeof(FhirTransactionResponseEntry)); - - bodyExpression = Expression.Assign(propertyExpression, Expression.Call(null, singleMethod, enumerableParameterExpression)); - } - - return Expression.Lambda>>(bodyExpression, parameterExpression, enumerableParameterExpression).Compile(); - } - - /// - public IReadOnlyList PropertyAccessors => _propertiesAccessors; -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/FhirTransactionResponse.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/FhirTransactionResponse.cs deleted file mode 100644 index 30588b8b64..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/FhirTransactionResponse.cs +++ /dev/null @@ -1,25 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -/// -/// Provides list of responses returned by executing the FHIR transaction. -/// -public class FhirTransactionResponse : IFhirTransactionRequestResponse -{ - /// - public FhirTransactionResponseEntry Patient { get; set; } - - /// - public FhirTransactionResponseEntry Endpoint { get; set; } - - /// - public FhirTransactionResponseEntry ImagingStudy { get; set; } - - public IEnumerable Observation { get; set; } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/FhirTransactionResponseEntry.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/FhirTransactionResponseEntry.cs deleted file mode 100644 index 6b3e6ad106..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/FhirTransactionResponseEntry.cs +++ /dev/null @@ -1,35 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using Hl7.Fhir.Model; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -/// -/// Provides a FHIR transaction response detail. -/// -public class FhirTransactionResponseEntry -{ - public FhirTransactionResponseEntry( - Bundle.ResponseComponent response, - Resource resource) - { - EnsureArg.IsNotNull(response, nameof(response)); - - Response = response; - Resource = resource; - } - - /// - /// Gets the response component. - /// - public Bundle.ResponseComponent Response { get; } - - /// - /// Gets the response resource. - /// - public Resource Resource { get; } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/IFhirTransactionContext.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/IFhirTransactionContext.cs deleted file mode 100644 index 40a534b11e..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/IFhirTransactionContext.cs +++ /dev/null @@ -1,29 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Microsoft.Health.Dicom.Client.Models; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -/// -/// Provides the context of a FHIR transaction. -/// -public interface IFhirTransactionContext -{ - /// - /// Gets the change feed used for this transaction. - /// - ChangeFeedEntry ChangeFeedEntry { get; } - - /// - /// Gets the request. - /// - FhirTransactionRequest Request { get; } - - /// - /// Gets the response. - /// - FhirTransactionResponse Response { get; } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/IFhirTransactionPipeline.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/IFhirTransactionPipeline.cs deleted file mode 100644 index eff252c9b4..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/IFhirTransactionPipeline.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Health.Dicom.Client.Models; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -/// -/// Provides functionality to process a FHIR transaction. -/// -public interface IFhirTransactionPipeline -{ - /// - /// Asynchronously processes a FHIR transaction from . - /// - /// The change feed entry to process. - /// The cancellation token. - /// A task that represents the asynchronous processing operation. - Task ProcessAsync(ChangeFeedEntry changeFeedEntry, CancellationToken cancellationToken); -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/IFhirTransactionPipelineStep.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/IFhirTransactionPipelineStep.cs deleted file mode 100644 index f647b4111d..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/IFhirTransactionPipelineStep.cs +++ /dev/null @@ -1,29 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using Task = System.Threading.Tasks.Task; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -/// -/// Provides functionality to prepare request for and process response of a FHIR transaction. -/// -public interface IFhirTransactionPipelineStep -{ - /// - /// Asynchronously prepares the transaction request. - /// - /// The transaction context. - /// The cancellation token. - /// A task representing the asynchronous preparation operation. - Task PrepareRequestAsync(FhirTransactionContext context, CancellationToken cancellationToken = default); - - /// - /// Processes the transaction response. - /// - /// The transaction context. - void ProcessResponse(FhirTransactionContext context); -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/IFhirTransactionRequestResponse.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/IFhirTransactionRequestResponse.cs deleted file mode 100644 index ab6ab82ebb..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/IFhirTransactionRequestResponse.cs +++ /dev/null @@ -1,35 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -/// -/// Provides list of request/responses used to execute or returned by executing the FHIR transaction. -/// -/// The type of the object used by the request or response. -public interface IFhirTransactionRequestResponse -{ - /// - /// Gets or sets the patient. - /// - T Patient { get; set; } - - /// - /// Gets or sets the endpoint to DicomWeb used by ImagingStudy. - /// - T Endpoint { get; set; } - - /// - /// Gets or sets the imaging study. - /// - T ImagingStudy { get; set; } - - /// - /// Gets or sets the observation - /// - IEnumerable Observation { get; set; } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/IFhirTransactionRequestResponsePropertyAccessors.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/IFhirTransactionRequestResponsePropertyAccessors.cs deleted file mode 100644 index 8e3f0d4b03..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/IFhirTransactionRequestResponsePropertyAccessors.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -/// -/// Provides property accessors for and . -/// -public interface IFhirTransactionRequestResponsePropertyAccessors -{ - /// - /// Gets list of property accessors for and . - /// - IReadOnlyList PropertyAccessors { get; } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ImagingStudy/IImagingStudyDeleteHandler.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ImagingStudy/IImagingStudyDeleteHandler.cs deleted file mode 100644 index 482922f96b..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ImagingStudy/IImagingStudyDeleteHandler.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using Hl7.Fhir.Model; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -/// -/// Builds the request for removing an instance from the resource. -/// -public interface IImagingStudyDeleteHandler -{ - /// - /// Builds a request for removing an instance from the resource. - /// - /// The transaction context. - /// The cancellation token. - /// The request entry. - Task BuildAsync(FhirTransactionContext context, CancellationToken cancellationToken); -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ImagingStudy/IImagingStudyInstancePropertySynchronizer.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ImagingStudy/IImagingStudyInstancePropertySynchronizer.cs deleted file mode 100644 index db2e3835a5..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ImagingStudy/IImagingStudyInstancePropertySynchronizer.cs +++ /dev/null @@ -1,21 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using Hl7.Fhir.Model; -using Task = System.Threading.Tasks.Task; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -public interface IImagingStudyInstancePropertySynchronizer -{ - /// - /// Synchronizes the DICOM properties to . - /// - /// The transaction context. - /// The instance component within study. - /// The cancellation token. - Task SynchronizeAsync(FhirTransactionContext context, ImagingStudy.InstanceComponent instance, CancellationToken cancellationToken); -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ImagingStudy/IImagingStudySeriesPropertySynchronizer.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ImagingStudy/IImagingStudySeriesPropertySynchronizer.cs deleted file mode 100644 index c93f84d24e..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ImagingStudy/IImagingStudySeriesPropertySynchronizer.cs +++ /dev/null @@ -1,21 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using Hl7.Fhir.Model; -using Task = System.Threading.Tasks.Task; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -public interface IImagingStudySeriesPropertySynchronizer -{ - /// - /// Synchronizes the DICOM properties to . - /// - /// The transaction context. - /// The series component within study. - /// The cancellation token. - Task SynchronizeAsync(FhirTransactionContext context, ImagingStudy.SeriesComponent series, CancellationToken cancellationToken); -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ImagingStudy/IImagingStudySynchronizer.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ImagingStudy/IImagingStudySynchronizer.cs deleted file mode 100644 index 97cf4a5f56..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ImagingStudy/IImagingStudySynchronizer.cs +++ /dev/null @@ -1,37 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using Hl7.Fhir.Model; -using Task = System.Threading.Tasks.Task; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -public interface IImagingStudySynchronizer -{ - /// - /// Synchronizes the DICOM properties to . - /// - /// The transaction context. - /// The resource. - /// The cancellation token. - Task SynchronizeStudyPropertiesAsync(FhirTransactionContext context, ImagingStudy imagingStudy, CancellationToken cancellationToken); - - /// - /// Synchronizes the DICOM properties to . - /// - /// The transaction context. - /// The resource. - /// The cancellation token. - Task SynchronizeSeriesPropertiesAsync(FhirTransactionContext context, ImagingStudy.SeriesComponent series, CancellationToken cancellationToken); - - /// - /// Synchronizes the DICOM properties to . - /// - /// The transaction context. - /// The resource. - /// The cancellation token. - Task SynchronizeInstancePropertiesAsync(FhirTransactionContext context, ImagingStudy.InstanceComponent instance, CancellationToken cancellationToken); -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ImagingStudy/IImagingStudyUpsertHandler.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ImagingStudy/IImagingStudyUpsertHandler.cs deleted file mode 100644 index d2f4f911d4..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ImagingStudy/IImagingStudyUpsertHandler.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using Hl7.Fhir.Model; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -/// -/// Builds the request for creating or updating the resource. -/// -public interface IImagingStudyUpsertHandler -{ - /// - /// Builds a request for creating or updating the resource.. - /// - /// The transaction context. - /// The cancellation token. - /// The request entry. - Task BuildAsync(FhirTransactionContext context, CancellationToken cancellationToken); -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ImagingStudy/IImagingStudypropertySynchronizer.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ImagingStudy/IImagingStudypropertySynchronizer.cs deleted file mode 100644 index 6de6841472..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ImagingStudy/IImagingStudypropertySynchronizer.cs +++ /dev/null @@ -1,21 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using Hl7.Fhir.Model; -using Task = System.Threading.Tasks.Task; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -public interface IImagingStudyPropertySynchronizer -{ - /// - /// Synchronizes the DICOM properties to . - /// - /// The transaction context. - /// The resource. - /// The cancellation token. - Task SynchronizeAsync(FhirTransactionContext context, ImagingStudy imagingStudy, CancellationToken cancellationToken = default); -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ImagingStudy/ImagingStudyDeleteHandler.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ImagingStudy/ImagingStudyDeleteHandler.cs deleted file mode 100644 index e4221defd2..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ImagingStudy/ImagingStudyDeleteHandler.cs +++ /dev/null @@ -1,89 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Hl7.Fhir.Model; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Client.Models; -using Microsoft.Health.DicomCast.Core.Configurations; -using Microsoft.Health.DicomCast.Core.Extensions; -using Microsoft.Health.DicomCast.Core.Features.Fhir; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -/// -/// Builds the request for creating or updating the resource. -/// -public class ImagingStudyDeleteHandler : IImagingStudyDeleteHandler -{ - private readonly IFhirService _fhirService; - private readonly string _dicomWebEndpoint; - - public ImagingStudyDeleteHandler( - IFhirService fhirService, - IOptions dicomWebConfiguration) - { - EnsureArg.IsNotNull(fhirService, nameof(fhirService)); - EnsureArg.IsNotNull(dicomWebConfiguration?.Value, nameof(dicomWebConfiguration)); - - _fhirService = fhirService; - _dicomWebEndpoint = dicomWebConfiguration.Value.Endpoint.ToString(); - } - - /// - public async Task BuildAsync(FhirTransactionContext context, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(context, nameof(context)); - EnsureArg.IsNotNull(context.ChangeFeedEntry, nameof(context.ChangeFeedEntry)); - - ChangeFeedEntry changeFeedEntry = context.ChangeFeedEntry; - - Identifier imagingStudyIdentifier = IdentifierUtility.CreateIdentifier(changeFeedEntry.StudyInstanceUid); - ImagingStudy imagingStudy = await _fhirService.RetrieveImagingStudyAsync(imagingStudyIdentifier, cancellationToken); - - // Returns null if imagingStudy does not exists for given studyInstanceUid - if (imagingStudy == null) - { - return null; - } - - string imagingStudySource = imagingStudy.Meta.Source; - - ImagingStudy.SeriesComponent series = ImagingStudyPipelineHelper.GetSeriesWithinAStudy(changeFeedEntry.SeriesInstanceUid, imagingStudy.Series); - ImagingStudy.InstanceComponent instance = ImagingStudyPipelineHelper.GetInstanceWithinASeries(changeFeedEntry.SopInstanceUid, series); - - // Return null if the given instance is not present in ImagingStudy - if (instance == null) - { - return null; - } - - // Removes instance from series collection - series.Instance.Remove(instance); - - // Removes series from ImagingStudy if its instance collection is empty - if (series.Instance.Count == 0) - { - imagingStudy.Series.Remove(series); - } - - if (imagingStudy.Series.Count == 0 && _dicomWebEndpoint.Equals(imagingStudySource, System.StringComparison.Ordinal)) - { - return new FhirTransactionRequestEntry( - FhirTransactionRequestMode.Delete, - ImagingStudyPipelineHelper.GenerateDeleteRequest(imagingStudy), - imagingStudy.ToServerResourceId(), - imagingStudy); - } - - return new FhirTransactionRequestEntry( - FhirTransactionRequestMode.Update, - ImagingStudyPipelineHelper.GenerateUpdateRequest(imagingStudy), - imagingStudy.ToServerResourceId(), - imagingStudy); - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ImagingStudy/ImagingStudyInstancePropertySynchronizer.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ImagingStudy/ImagingStudyInstancePropertySynchronizer.cs deleted file mode 100644 index c61907ba50..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ImagingStudy/ImagingStudyInstancePropertySynchronizer.cs +++ /dev/null @@ -1,78 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Threading; -using EnsureThat; -using FellowOakDicom; -using Hl7.Fhir.Model; -using Microsoft.Extensions.Options; -using Microsoft.Health.DicomCast.Core.Configurations; -using Microsoft.Health.DicomCast.Core.Features.ExceptionStorage; -using Task = System.Threading.Tasks.Task; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -public class ImagingStudyInstancePropertySynchronizer : IImagingStudyInstancePropertySynchronizer -{ - private readonly DicomCastConfiguration _dicomCastConfiguration; - private readonly IExceptionStore _exceptionStore; - private readonly IEnumerable<(Action PropertyAction, bool RequiredProperty)> _propertiesToSync = new List<(Action PropertyAction, bool RequiredProperty)>() - { - (AddSopClass, true), - (AddInstanceNumber, false), - }; - - public ImagingStudyInstancePropertySynchronizer( - IOptions dicomCastConfiguration, - IExceptionStore exceptionStore) - { - EnsureArg.IsNotNull(dicomCastConfiguration, nameof(dicomCastConfiguration)); - EnsureArg.IsNotNull(exceptionStore, nameof(exceptionStore)); - - _dicomCastConfiguration = dicomCastConfiguration.Value; - _exceptionStore = exceptionStore; - } - - /// - public async Task SynchronizeAsync(FhirTransactionContext context, ImagingStudy.InstanceComponent instance, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(context, nameof(context)); - EnsureArg.IsNotNull(context.ChangeFeedEntry, nameof(context.ChangeFeedEntry)); - EnsureArg.IsNotNull(instance, nameof(instance)); - - DicomDataset dataset = context.ChangeFeedEntry.Metadata; - - if (dataset == null) - { - return; - } - - foreach (var property in _propertiesToSync) - { - await ImagingStudyPipelineHelper.SynchronizePropertiesAsync(instance, context, property.PropertyAction, property.RequiredProperty, _dicomCastConfiguration.Features.EnforceValidationOfTagValues, _exceptionStore, cancellationToken); - } - } - - private static void AddSopClass(ImagingStudy.InstanceComponent instance, FhirTransactionContext context) - { - DicomDataset dataset = context.ChangeFeedEntry.Metadata; - if (dataset.TryGetSingleValue(DicomTag.SOPClassUID, out string sopClassUid) && - !string.Equals(instance.SopClass?.Code, sopClassUid, StringComparison.Ordinal)) - { - instance.SopClass = new Coding(null, sopClassUid); - } - } - - private static void AddInstanceNumber(ImagingStudy.InstanceComponent instance, FhirTransactionContext context) - { - DicomDataset dataset = context.ChangeFeedEntry.Metadata; - if (dataset.TryGetSingleValue(DicomTag.InstanceNumber, out int instanceNumber)) - { - instance.Number = instanceNumber; - } - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ImagingStudy/ImagingStudyPipelineHelper.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ImagingStudy/ImagingStudyPipelineHelper.cs deleted file mode 100644 index 76d93b3a22..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ImagingStudy/ImagingStudyPipelineHelper.cs +++ /dev/null @@ -1,166 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Net.Http.Headers; -using System.Threading; -using EnsureThat; -using FellowOakDicom; -using Hl7.Fhir.Model; -using Hl7.Fhir.Utility; -using Microsoft.Health.DicomCast.Core.Exceptions; -using Microsoft.Health.DicomCast.Core.Extensions; -using Microsoft.Health.DicomCast.Core.Features.ExceptionStorage; -using Task = System.Threading.Tasks.Task; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -public static class ImagingStudyPipelineHelper -{ - private const StringComparison EqualsStringComparison = StringComparison.Ordinal; - - public static ImagingStudy.InstanceComponent GetInstanceWithinASeries(string sopInstanceUid, ImagingStudy.SeriesComponent existingSeries) - { - if (existingSeries == null) - { - return null; - } - - return existingSeries.Instance.FirstOrDefault(instance => sopInstanceUid.Equals(instance.Uid, EqualsStringComparison)); - } - - public static ImagingStudy.SeriesComponent GetSeriesWithinAStudy(string seriesInstanceUid, IEnumerable existingSeriesCollection) - { - if (existingSeriesCollection == null) - { - return null; - } - - return existingSeriesCollection.FirstOrDefault(series => seriesInstanceUid.Equals(series.Uid, EqualsStringComparison)); - } - - public static string GenerateEtag(string versionId) - { - var eTag = new EntityTagHeaderValue($"\"{versionId}\"", true); - return eTag.ToString(); - } - - public static Bundle.RequestComponent GenerateCreateRequest(Identifier imagingStudyIdentifier) - { - return new Bundle.RequestComponent() - { - Method = Bundle.HTTPVerb.POST, - IfNoneExist = imagingStudyIdentifier.ToSearchQueryParameter(), - Url = ResourceType.ImagingStudy.GetLiteral(), - }; - } - - public static Bundle.RequestComponent GenerateUpdateRequest(ImagingStudy imagingStudy) - { - EnsureArg.IsNotNull(imagingStudy, nameof(imagingStudy)); - - return new Bundle.RequestComponent() - { - Method = Bundle.HTTPVerb.PUT, - IfMatch = GenerateEtag(imagingStudy.Meta.VersionId), - Url = $"{ResourceType.ImagingStudy.GetLiteral()}/{imagingStudy.Id}", - }; - } - - public static Bundle.RequestComponent GenerateDeleteRequest(ImagingStudy imagingStudy) - { - EnsureArg.IsNotNull(imagingStudy, nameof(imagingStudy)); - - return new Bundle.RequestComponent() - { - Method = Bundle.HTTPVerb.DELETE, - Url = $"{ResourceType.ImagingStudy.GetLiteral()}/{imagingStudy.Id}", - }; - } - - public static string GetModalityInString(DicomDataset dataset) - { - EnsureArg.IsNotNull(dataset, nameof(dataset)); - return dataset.GetSingleValueOrDefault(DicomTag.Modality, default); - } - - public static string GetAccessionNumberInString(DicomDataset dataset) - { - EnsureArg.IsNotNull(dataset, nameof(dataset)); - return dataset.GetSingleValueOrDefault(DicomTag.AccessionNumber, default); - } - - public static Identifier GetAccessionNumber(string accessionNumber) - { - EnsureArg.IsNotNull(accessionNumber, nameof(accessionNumber)); - var coding = new Coding(system: FhirTransactionConstants.AccessionNumberTypeSystem, code: FhirTransactionConstants.AccessionNumberTypeCode); - var codeableConcept = new CodeableConcept(); - codeableConcept.Coding.Add(coding); - return new Identifier(system: null, value: accessionNumber) - { - Type = codeableConcept, - }; - } - - public static Coding GetModality(string modalityInString) - { - if (modalityInString != null) - { - return new Coding(FhirTransactionConstants.ModalityInSystem, modalityInString); - } - - return null; - } - - public static void SetDateTimeOffSet(FhirTransactionContext context) - { - EnsureArg.IsNotNull(context, nameof(context)); - - DicomDataset metadata = context.ChangeFeedEntry.Metadata; - - if (metadata != null && - metadata.TryGetSingleValue(DicomTag.TimezoneOffsetFromUTC, out string utcOffsetInString)) - { - try - { - context.UtcDateTimeOffset = DateTimeOffset.ParseExact(utcOffsetInString, FhirTransactionConstants.UtcTimezoneOffsetFormat, CultureInfo.InvariantCulture, DateTimeStyles.AllowWhiteSpaces).Offset; - } - catch (FormatException) - { - throw new InvalidDicomTagValueException(nameof(DicomTag.TimezoneOffsetFromUTC), utcOffsetInString); - } - } - } - - public static async Task SynchronizePropertiesAsync(T component, FhirTransactionContext context, Action synchronizeAction, bool requiredProperty, bool enforceAllFields, IExceptionStore exceptionStore, CancellationToken cancellationToken = default) - { - EnsureArg.IsNotNull(context, nameof(context)); - EnsureArg.IsNotNull(synchronizeAction, nameof(synchronizeAction)); - EnsureArg.IsNotNull(exceptionStore, nameof(exceptionStore)); - - try - { - synchronizeAction(component, context); - } - catch (DicomTagException ex) - { - if (!enforceAllFields && !requiredProperty) - { - await exceptionStore.WriteExceptionAsync( - context.ChangeFeedEntry, - ex, - ErrorType.DicomValidationError, - cancellationToken); - } - else - { - throw; - } - } - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ImagingStudy/ImagingStudyPipelineStep.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ImagingStudy/ImagingStudyPipelineStep.cs deleted file mode 100644 index bada7c8fcb..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ImagingStudy/ImagingStudyPipelineStep.cs +++ /dev/null @@ -1,81 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Globalization; -using System.Net; -using System.Threading; -using EnsureThat; -using Hl7.Fhir.Model; -using Hl7.Fhir.Utility; -using Microsoft.Extensions.Logging; -using Microsoft.Health.Dicom.Client.Models; -using Microsoft.Health.DicomCast.Core.Features.Fhir; -using Task = System.Threading.Tasks.Task; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -/// -/// Pipeline step for handling . -/// -public class ImagingStudyPipelineStep : FhirTransactionPipelineStepBase -{ - private readonly IImagingStudyUpsertHandler _imagingStudyUpsertHandler; - private readonly IImagingStudyDeleteHandler _imagingStudyDeleteHandler; - - public ImagingStudyPipelineStep( - IImagingStudyUpsertHandler imagingStudyUpsertHandler, - IImagingStudyDeleteHandler imagingStudyDeleteHandler, - ILogger logger) - : base(logger) - { - EnsureArg.IsNotNull(imagingStudyUpsertHandler, nameof(imagingStudyUpsertHandler)); - EnsureArg.IsNotNull(imagingStudyDeleteHandler, nameof(imagingStudyDeleteHandler)); - - _imagingStudyUpsertHandler = imagingStudyUpsertHandler; - _imagingStudyDeleteHandler = imagingStudyDeleteHandler; - } - - /// - protected override async Task PrepareRequestImplementationAsync(FhirTransactionContext context, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(context, nameof(context)); - - ChangeFeedEntry changeFeedEntry = context.ChangeFeedEntry; - - context.Request.ImagingStudy = changeFeedEntry.Action switch - { - ChangeFeedAction.Create => await _imagingStudyUpsertHandler.BuildAsync(context, cancellationToken), - ChangeFeedAction.Delete => await _imagingStudyDeleteHandler.BuildAsync(context, cancellationToken), - _ => throw new NotSupportedException( - string.Format( - CultureInfo.InvariantCulture, - DicomCastCoreResource.NotSupportedChangeFeedAction, - changeFeedEntry.Action)), - }; - } - - /// - protected override void ProcessResponseImplementation(FhirTransactionContext context) - { - EnsureArg.IsNotNull(context, nameof(context)); - - // If the ImagingStudy does not exist, we will use conditional create to create the resource - // to avoid duplicated resource being created. However, if the resource with the identifier - // was created externally between the retrieve and create, conditional create will return 200 - // and might not contain the changes so we will need to try again. - if (context.Request.ImagingStudy?.RequestMode == FhirTransactionRequestMode.Create) - { - FhirTransactionResponseEntry imagingStudy = context.Response.ImagingStudy; - - HttpStatusCode statusCode = imagingStudy.Response.Annotation(); - - if (statusCode == HttpStatusCode.OK) - { - throw new ResourceConflictException(); - } - } - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ImagingStudy/ImagingStudyPropertySynchronizer.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ImagingStudy/ImagingStudyPropertySynchronizer.cs deleted file mode 100644 index 264137b5c4..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ImagingStudy/ImagingStudyPropertySynchronizer.cs +++ /dev/null @@ -1,134 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using EnsureThat; -using FellowOakDicom; -using Hl7.Fhir.Model; -using Microsoft.Extensions.Options; -using Microsoft.Health.DicomCast.Core.Configurations; -using Microsoft.Health.DicomCast.Core.Extensions; -using Microsoft.Health.DicomCast.Core.Features.ExceptionStorage; -using Task = System.Threading.Tasks.Task; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -public class ImagingStudyPropertySynchronizer : IImagingStudyPropertySynchronizer -{ - private readonly DicomCastConfiguration _dicomCastConfiguration; - private readonly IExceptionStore _exceptionStore; - private readonly IEnumerable<(Action PropertyAction, bool RequiredProperty)> _propertiesToSync = new List<(Action, bool)>() - { - (AddStartedElement, false), - (AddImagingStudyEndpoint, false), - (AddModality, false), - (AddNote, false), - (AddAccessionNumber, false), - }; - - public ImagingStudyPropertySynchronizer( - IOptions dicomCastConfiguration, - IExceptionStore exceptionStore) - { - EnsureArg.IsNotNull(dicomCastConfiguration, nameof(dicomCastConfiguration)); - EnsureArg.IsNotNull(exceptionStore, nameof(exceptionStore)); - - _dicomCastConfiguration = dicomCastConfiguration.Value; - _exceptionStore = exceptionStore; - } - - /// - public async Task SynchronizeAsync(FhirTransactionContext context, ImagingStudy imagingStudy, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(context, nameof(context)); - EnsureArg.IsNotNull(imagingStudy, nameof(imagingStudy)); - EnsureArg.IsNotNull(context.Request.Endpoint, nameof(context.Request.Endpoint)); - EnsureArg.IsNotNull(context.ChangeFeedEntry, nameof(context.ChangeFeedEntry)); - - DicomDataset dataset = context.ChangeFeedEntry.Metadata; - - if (dataset == null) - { - return; - } - - foreach (var property in _propertiesToSync) - { - await ImagingStudyPipelineHelper.SynchronizePropertiesAsync(imagingStudy, context, property.PropertyAction, property.RequiredProperty, _dicomCastConfiguration.Features.EnforceValidationOfTagValues, _exceptionStore, cancellationToken); - } - } - - private static void AddNote(ImagingStudy imagingStudy, FhirTransactionContext context) - { - DicomDataset dataset = context.ChangeFeedEntry.Metadata; - if (dataset.TryGetSingleValue(DicomTag.StudyDescription, out string description)) - { - if (!imagingStudy.Note.Any(note => string.Equals(note.Text.Value, description, StringComparison.Ordinal))) - { - Annotation annotation = new Annotation() - { - Text = new Markdown(description), - }; - - imagingStudy.Note.Add(annotation); - } - } - } - - private static void AddImagingStudyEndpoint(ImagingStudy imagingStudy, FhirTransactionContext context) - { - var endpointReference = context.Request.Endpoint.ResourceId.ToResourceReference(); - - if (!imagingStudy.Endpoint.Any(endpoint => endpointReference.IsExactly(endpoint))) - { - imagingStudy.Endpoint.Add(endpointReference); - } - } - - private static void AddStartedElement(ImagingStudy imagingStudy, FhirTransactionContext context) - { - ImagingStudyPipelineHelper.SetDateTimeOffSet(context); - DicomDataset dataset = context.ChangeFeedEntry.Metadata; - TimeSpan utcOffset = context.UtcDateTimeOffset; - - imagingStudy.StartedElement = dataset.GetDateTimePropertyIfNotDefaultValue(DicomTag.StudyDate, DicomTag.StudyTime, utcOffset); - } - - private static void AddModality(ImagingStudy imagingStudy, FhirTransactionContext context) - { - DicomDataset dataset = context.ChangeFeedEntry.Metadata; - string modalityInString = ImagingStudyPipelineHelper.GetModalityInString(dataset); - - if (modalityInString != null) - { - Coding modality = ImagingStudyPipelineHelper.GetModality(modalityInString); - - List existingModalities = imagingStudy.Modality; - - if (dataset.TryGetValues(DicomTag.ModalitiesInStudy, out string[] modalitiesInStudy) && - !existingModalities.Any(existingModality => string.Equals(existingModality.Code, modalityInString, StringComparison.OrdinalIgnoreCase))) - { - imagingStudy.Modality.Add(modality); - } - } - } - - private static void AddAccessionNumber(ImagingStudy imagingStudy, FhirTransactionContext context) - { - DicomDataset dataset = context.ChangeFeedEntry.Metadata; - string accessionNumber = ImagingStudyPipelineHelper.GetAccessionNumberInString(dataset); - if (accessionNumber != null) - { - Identifier accessionNumberId = ImagingStudyPipelineHelper.GetAccessionNumber(accessionNumber); - if (!imagingStudy.Identifier.Any(item => accessionNumberId.IsExactly(item))) - { - imagingStudy.Identifier.Add(accessionNumberId); - } - } - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ImagingStudy/ImagingStudySeriesPropertySynchronizer.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ImagingStudy/ImagingStudySeriesPropertySynchronizer.cs deleted file mode 100644 index 996cd7ca47..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ImagingStudy/ImagingStudySeriesPropertySynchronizer.cs +++ /dev/null @@ -1,100 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Threading; -using EnsureThat; -using FellowOakDicom; -using Hl7.Fhir.Model; -using Microsoft.Extensions.Options; -using Microsoft.Health.DicomCast.Core.Configurations; -using Microsoft.Health.DicomCast.Core.Extensions; -using Microsoft.Health.DicomCast.Core.Features.ExceptionStorage; -using Task = System.Threading.Tasks.Task; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -public class ImagingStudySeriesPropertySynchronizer : IImagingStudySeriesPropertySynchronizer -{ - private readonly DicomCastConfiguration _dicomCastConfiguration; - private readonly IExceptionStore _exceptionStore; - private readonly IEnumerable<(Action PropertyAction, bool RequiredProperty)> _propertiesToSync = new List<(Action, bool)>() - { - (AddSeriesNumber, false), - (AddDescription, false), - (AddModalityToSeries, true), - (AddStartedElement, false), - }; - - public ImagingStudySeriesPropertySynchronizer( - IOptions dicomCastConfiguration, - IExceptionStore exceptionStore) - { - EnsureArg.IsNotNull(dicomCastConfiguration, nameof(dicomCastConfiguration)); - EnsureArg.IsNotNull(exceptionStore, nameof(exceptionStore)); - - _dicomCastConfiguration = dicomCastConfiguration.Value; - _exceptionStore = exceptionStore; - } - - /// - public async Task SynchronizeAsync(FhirTransactionContext context, ImagingStudy.SeriesComponent series, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(context, nameof(context)); - EnsureArg.IsNotNull(context.ChangeFeedEntry, nameof(context.ChangeFeedEntry)); - EnsureArg.IsNotNull(series, nameof(series)); - - DicomDataset dataset = context.ChangeFeedEntry.Metadata; - - if (dataset == null) - { - return; - } - - foreach (var property in _propertiesToSync) - { - await ImagingStudyPipelineHelper.SynchronizePropertiesAsync(series, context, property.PropertyAction, property.RequiredProperty, _dicomCastConfiguration.Features.EnforceValidationOfTagValues, _exceptionStore, cancellationToken); - } - } - - private static void AddSeriesNumber(ImagingStudy.SeriesComponent series, FhirTransactionContext context) - { - DicomDataset dataset = context.ChangeFeedEntry.Metadata; - if (dataset.TryGetSingleValue(DicomTag.SeriesNumber, out int seriesNumber)) - { - series.Number = seriesNumber; - } - } - - private static void AddDescription(ImagingStudy.SeriesComponent series, FhirTransactionContext context) - { - DicomDataset dataset = context.ChangeFeedEntry.Metadata; - if (dataset.TryGetSingleValue(DicomTag.SeriesDescription, out string description)) - { - series.Description = description; - } - } - - // Add startedElement to series - private static void AddStartedElement(ImagingStudy.SeriesComponent series, FhirTransactionContext context) - { - DicomDataset dataset = context.ChangeFeedEntry.Metadata; - TimeSpan utcOffset = context.UtcDateTimeOffset; - series.StartedElement = dataset.GetDateTimePropertyIfNotDefaultValue(DicomTag.SeriesDate, DicomTag.SeriesTime, utcOffset); - } - - // Add modality to series - private static void AddModalityToSeries(ImagingStudy.SeriesComponent series, FhirTransactionContext context) - { - DicomDataset dataset = context.ChangeFeedEntry.Metadata; - string modalityInString = ImagingStudyPipelineHelper.GetModalityInString(dataset); - - if (modalityInString != null && !string.Equals(series.Modality?.Code, modalityInString, StringComparison.Ordinal)) - { - series.Modality = ImagingStudyPipelineHelper.GetModality(modalityInString); - } - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ImagingStudy/ImagingStudySynchronizer.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ImagingStudy/ImagingStudySynchronizer.cs deleted file mode 100644 index 5fdc60c4a2..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ImagingStudy/ImagingStudySynchronizer.cs +++ /dev/null @@ -1,56 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using EnsureThat; -using Hl7.Fhir.Model; -using Task = System.Threading.Tasks.Task; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -public class ImagingStudySynchronizer : IImagingStudySynchronizer -{ - private readonly IImagingStudyPropertySynchronizer _imagingStudyPropertySynchronizer; - private readonly IImagingStudySeriesPropertySynchronizer _imagingStudySeriesPropertySynchronizer; - private readonly IImagingStudyInstancePropertySynchronizer _imagingStudyInstancePropertySynchronizer; - - public ImagingStudySynchronizer( - IImagingStudyPropertySynchronizer imagingStudyPropertySynchronizer, - IImagingStudySeriesPropertySynchronizer imagingStudySeriesPropertySynchronizer, - IImagingStudyInstancePropertySynchronizer imagingStudyInstancePropertySynchronizer) - { - EnsureArg.IsNotNull(imagingStudyPropertySynchronizer, nameof(imagingStudyPropertySynchronizer)); - EnsureArg.IsNotNull(imagingStudySeriesPropertySynchronizer, nameof(imagingStudySeriesPropertySynchronizer)); - EnsureArg.IsNotNull(imagingStudyInstancePropertySynchronizer, nameof(imagingStudyInstancePropertySynchronizer)); - - _imagingStudyPropertySynchronizer = imagingStudyPropertySynchronizer; - _imagingStudySeriesPropertySynchronizer = imagingStudySeriesPropertySynchronizer; - _imagingStudyInstancePropertySynchronizer = imagingStudyInstancePropertySynchronizer; - } - - public async Task SynchronizeStudyPropertiesAsync(FhirTransactionContext context, ImagingStudy imagingStudy, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(context, nameof(context)); - EnsureArg.IsNotNull(imagingStudy, nameof(imagingStudy)); - - await _imagingStudyPropertySynchronizer.SynchronizeAsync(context, imagingStudy, cancellationToken); - } - - public async Task SynchronizeSeriesPropertiesAsync(FhirTransactionContext context, ImagingStudy.SeriesComponent series, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(context, nameof(context)); - EnsureArg.IsNotNull(series, nameof(series)); - - await _imagingStudySeriesPropertySynchronizer.SynchronizeAsync(context, series, cancellationToken); - } - - public async Task SynchronizeInstancePropertiesAsync(FhirTransactionContext context, ImagingStudy.InstanceComponent instance, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(context, nameof(context)); - EnsureArg.IsNotNull(instance, nameof(instance)); - - await _imagingStudyInstancePropertySynchronizer.SynchronizeAsync(context, instance, cancellationToken); - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ImagingStudy/ImagingStudyUpsertHandler.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ImagingStudy/ImagingStudyUpsertHandler.cs deleted file mode 100644 index cbe375689a..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ImagingStudy/ImagingStudyUpsertHandler.cs +++ /dev/null @@ -1,153 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Hl7.Fhir.Model; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Client.Models; -using Microsoft.Health.DicomCast.Core.Configurations; -using Microsoft.Health.DicomCast.Core.Extensions; -using Microsoft.Health.DicomCast.Core.Features.Fhir; -using Task = System.Threading.Tasks.Task; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -/// -/// Builds the request for creating or updating the resource. -/// -public class ImagingStudyUpsertHandler : IImagingStudyUpsertHandler -{ - private readonly IFhirService _fhirService; - private readonly IImagingStudySynchronizer _imagingStudySynchronizer; - private readonly string _dicomWebEndpoint; - - public ImagingStudyUpsertHandler( - IFhirService fhirService, - IImagingStudySynchronizer imagingStudySynchronizer, - IOptions dicomWebConfiguration) - { - EnsureArg.IsNotNull(fhirService, nameof(fhirService)); - EnsureArg.IsNotNull(imagingStudySynchronizer, nameof(_imagingStudySynchronizer)); - EnsureArg.IsNotNull(dicomWebConfiguration?.Value, nameof(dicomWebConfiguration)); - - _fhirService = fhirService; - _imagingStudySynchronizer = imagingStudySynchronizer; - _dicomWebEndpoint = dicomWebConfiguration.Value.Endpoint.ToString(); - } - - /// - public async Task BuildAsync(FhirTransactionContext context, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(context, nameof(context)); - EnsureArg.IsNotNull(context.ChangeFeedEntry, nameof(context.ChangeFeedEntry)); - EnsureArg.IsNotNull(context.Request, nameof(context.Request)); - - IResourceId patientId = context.Request.Patient.ResourceId; - - ChangeFeedEntry changeFeedEntry = context.ChangeFeedEntry; - - Identifier imagingStudyIdentifier = IdentifierUtility.CreateIdentifier(changeFeedEntry.StudyInstanceUid); - - ImagingStudy existingImagingStudy = await _fhirService.RetrieveImagingStudyAsync(imagingStudyIdentifier, cancellationToken); - ImagingStudy imagingStudy = (ImagingStudy)existingImagingStudy?.DeepCopy(); - - FhirTransactionRequestMode requestMode = FhirTransactionRequestMode.None; - - if (existingImagingStudy == null) - { - imagingStudy = new ImagingStudy() - { - Status = ImagingStudy.ImagingStudyStatus.Available, - Subject = patientId.ToResourceReference(), - }; - - imagingStudy.Identifier.Add(imagingStudyIdentifier); - imagingStudy.Meta = new Meta() - { - Source = _dicomWebEndpoint, - }; - requestMode = FhirTransactionRequestMode.Create; - } - - await SynchronizeImagingStudyPropertiesAsync(context, imagingStudy, cancellationToken); - - if (requestMode != FhirTransactionRequestMode.Create && - !existingImagingStudy.IsExactly(imagingStudy)) - { - requestMode = FhirTransactionRequestMode.Update; - } - - Bundle.RequestComponent request = requestMode switch - { - FhirTransactionRequestMode.Create => ImagingStudyPipelineHelper.GenerateCreateRequest(imagingStudyIdentifier), - FhirTransactionRequestMode.Update => ImagingStudyPipelineHelper.GenerateUpdateRequest(imagingStudy), - _ => null, - }; - - IResourceId resourceId = requestMode switch - { - FhirTransactionRequestMode.Create => new ClientResourceId(), - _ => existingImagingStudy.ToServerResourceId(), - }; - - return new FhirTransactionRequestEntry( - requestMode, - request, - resourceId, - imagingStudy); - } - - private async Task SynchronizeImagingStudyPropertiesAsync(FhirTransactionContext context, ImagingStudy imagingStudy, CancellationToken cancellationToken) - { - await _imagingStudySynchronizer.SynchronizeStudyPropertiesAsync(context, imagingStudy, cancellationToken); - - await AddSeriesToImagingStudyAsync(context, imagingStudy, cancellationToken); - } - - private async Task AddSeriesToImagingStudyAsync(FhirTransactionContext context, ImagingStudy imagingStudy, CancellationToken cancellationToken) - { - ChangeFeedEntry changeFeedEntry = context.ChangeFeedEntry; - - List existingSeriesCollection = imagingStudy.Series; - - ImagingStudy.InstanceComponent instance = new ImagingStudy.InstanceComponent() - { - Uid = changeFeedEntry.SopInstanceUid, - }; - - // Checks if the given series already exists within a study - ImagingStudy.SeriesComponent series = ImagingStudyPipelineHelper.GetSeriesWithinAStudy(changeFeedEntry.SeriesInstanceUid, existingSeriesCollection); - - if (series == null) - { - series = new ImagingStudy.SeriesComponent() - { - Uid = changeFeedEntry.SeriesInstanceUid, - }; - - series.Instance.Add(instance); - imagingStudy.Series.Add(series); - } - else - { - ImagingStudy.InstanceComponent existingInstance = ImagingStudyPipelineHelper.GetInstanceWithinASeries(changeFeedEntry.SopInstanceUid, series); - - if (existingInstance == null) - { - series.Instance.Add(instance); - } - else - { - instance = existingInstance; - } - } - - await _imagingStudySynchronizer.SynchronizeSeriesPropertiesAsync(context, series, cancellationToken); - await _imagingStudySynchronizer.SynchronizeInstancePropertiesAsync(context, instance, cancellationToken); - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/InvalidDicomTagValueException.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/InvalidDicomTagValueException.cs deleted file mode 100644 index 947cb871ff..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/InvalidDicomTagValueException.cs +++ /dev/null @@ -1,28 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Globalization; -using EnsureThat; -using Microsoft.Health.DicomCast.Core.Exceptions; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -/// -/// Exception thrown when a DICOM tag value is invalid. -/// -public class InvalidDicomTagValueException : DicomTagException -{ - public InvalidDicomTagValueException(string tagName, string value) - : base(FormatMessage(tagName, value)) - { - } - - private static string FormatMessage(string tagName, string value) - { - EnsureArg.IsNotNullOrWhiteSpace(tagName, nameof(tagName)); - - return string.Format(CultureInfo.InvariantCulture, DicomCastCoreResource.InvalidDicomTagValue, value, tagName); - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/MissingRequiredDicomTagException.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/MissingRequiredDicomTagException.cs deleted file mode 100644 index 929de01334..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/MissingRequiredDicomTagException.cs +++ /dev/null @@ -1,28 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Globalization; -using EnsureThat; -using Microsoft.Health.DicomCast.Core.Exceptions; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -/// -/// Exception thrown when required DICOM tag is missing. -/// -public class MissingRequiredDicomTagException : DicomTagException -{ - public MissingRequiredDicomTagException(string dicomTagName) - : base(FormatMessage(dicomTagName)) - { - } - - private static string FormatMessage(string dicomTagName) - { - EnsureArg.IsNotNullOrWhiteSpace(dicomTagName, nameof(dicomTagName)); - - return string.Format(CultureInfo.CurrentCulture, DicomCastCoreResource.MissingRequiredDicomTag, dicomTagName); - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/Observation/IObservationDeleteHandler.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/Observation/IObservationDeleteHandler.cs deleted file mode 100644 index e86cadffa6..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/Observation/IObservationDeleteHandler.cs +++ /dev/null @@ -1,15 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -public interface IObservationDeleteHandler -{ - Task> BuildAsync(FhirTransactionContext context, CancellationToken cancellationToken); -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/Observation/IObservationUpsertHandler.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/Observation/IObservationUpsertHandler.cs deleted file mode 100644 index a6cb9727c3..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/Observation/IObservationUpsertHandler.cs +++ /dev/null @@ -1,15 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -public interface IObservationUpsertHandler -{ - Task> BuildAsync(FhirTransactionContext context, CancellationToken cancellationToken); -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/Observation/ObservationConstants.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/Observation/ObservationConstants.cs deleted file mode 100644 index dbfd979436..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/Observation/ObservationConstants.cs +++ /dev/null @@ -1,95 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Immutable; -using FellowOakDicom.StructuredReport; -using Hl7.Fhir.Model; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -/// -/// Dicom structured report codes used by FHIR Observation profiles -/// -public static class ObservationConstants -{ - // https://www.hl7.org/fhir/terminologies-systems.html - public const string SctSystem = "http://snomed.info/sct"; - public const string DcmSystem = "http://dicom.nema.org/resources/ontology/DCM"; - - public const string Dcm = "DCM"; - public const string Sct = "Sct"; - public const string Ln = "LN"; - - //------------------------------------------------------------ - // Report codes - // - When you encounter these codes in a structured report, it means to create a new "Does Summary" Observation - //------------------------------------------------------------ - public static readonly DicomCodeItem RadiopharmaceuticalRadiationDoseReport = new("113500", Dcm, "Radiopharmaceutical Radiation Dose Report"); - public static readonly DicomCodeItem XRayRadiationDoseReport = new("113701", Dcm, "X-Ray Radiation Dose Report"); - - //------------------------------------------------------------ - // Irradiation Event Codes - // - When you encounter these code in a structured report, it means to create a new "Irradiation Event" Observation - //------------------------------------------------------------ - public static readonly DicomCodeItem IrradiationEventXRayData = new("113706", Dcm, "Irradiation Event X-Ray Data"); - public static readonly DicomCodeItem CtAcquisition = new("113819", Dcm, "CT Acquisition"); - public static readonly DicomCodeItem RadiopharmaceuticalAdministration = new("113502", Dcm, "Radiopharmaceutical Administration"); - - //------------------------------------------------------------ - // Dicom Codes (attribute) - // - These are report values which map to non component observation attributes. - //------------------------------------------------------------ - public static readonly DicomCodeItem IrradiationAuthorizingPerson = new("113850", Dcm, "Irradiation Authorizing"); - public static readonly DicomCodeItem PregnancyObservation = new("364320009", Sct, "Pregnancy observable"); - public static readonly DicomCodeItem IndicationObservation = new("18785-6", Ln, "Indications for Procedure"); - public static readonly DicomCodeItem IrradiatingDevice = new("113859", Dcm, "Irradiating Device"); - - public static readonly DicomCodeItem IrradiationEventUid = new("113769", Dcm, "Irradiation Event UID"); - public static readonly DicomCodeItem StudyInstanceUid = new("110180", Dcm, "Study Instance UID"); - public static readonly DicomCodeItem AccessionNumber = new("121022", Dcm, "Accession Number"); - public static readonly DicomCodeItem StartOfXrayIrradiation = new("113809", Dcm, "Start of X-Ray Irradiation”)"); - - //------------------------------------------------------------ - // Dicom codes (component) - // - These are report values which map to Observation.component values - //------------------------------------------------------------ - // Study - public static readonly DicomCodeItem DoseRpTotal = new("113725", Dcm, "Dose (RP) Total"); - public static readonly DicomCodeItem EntranceExposureAtRp = new("111636", Dcm, "Entrance Exposure at RP"); - public static readonly DicomCodeItem AccumulatedAverageGlandularDose = new("111637", Dcm, "Accumulated Average Glandular Dose"); - public static readonly DicomCodeItem DoseAreaProductTotal = new("113722", Dcm, "Dose Area Product Total"); - public static readonly DicomCodeItem FluoroDoseAreaProductTotal = new("113726", Dcm, "Fluoro Dose Area Product Total"); - public static readonly DicomCodeItem AcquisitionDoseAreaProductTotal = new("113727", Dcm, "Acquisition Dose Area Product Total"); - public static readonly DicomCodeItem TotalFluoroTime = new("113730", Dcm, "Total Fluoro Time"); - public static readonly DicomCodeItem TotalNumberOfRadiographicFrames = new("113731", Dcm, "Total Number of Radiographic Frames"); - public static readonly DicomCodeItem AdministeredActivity = new("113507", Dcm, "Administered activity"); - public static readonly DicomCodeItem CtDoseLengthProductTotal = new("113813", Dcm, "CT Dose Length Product Total"); - public static readonly DicomCodeItem TotalNumberOfIrradiationEvents = new("113812", Dcm, ""); - public static readonly DicomCodeItem RadiopharmaceuticalAgent = new("349358000", Sct, "Radiopharmaceutical agent"); - public static readonly DicomCodeItem Radionuclide = new("89457008", Sct, "Radionuclide"); - public static readonly DicomCodeItem RadiopharmaceuticalVolume = new("123005", Dcm, "Radiopharmaceutical Volume"); - public static readonly DicomCodeItem RouteOfAdministration = new("410675002", Sct, "Route of administration"); - - // (Ir)radiation Event - // uses MeanCtdIvol as well - public static readonly DicomCodeItem MeanCtdIvol = new("113830", Dcm, "Mean CTDIvol"); - public static readonly DicomCodeItem Dlp = new("113838", Dcm, "DLP"); - public static readonly DicomCodeItem TargetRegion = new("123014", Dcm, "Target Region"); - public static readonly DicomCodeItem CtdIwPhantomType = new("113835", Dcm, "CTDIw Phantom Type"); - - // FHIR CodeableConcepts - public static readonly CodeableConcept RadiationExposureCodeableConcept = new CodeableConcept("http://loinc.org", "73569-6", "Radiation exposure and protection information"); - public static readonly CodeableConcept AccessionCodeableConcept = new CodeableConcept("http://terminology.hl7.org/CodeSystem/v2-0203", "ACSN"); - public static readonly CodeableConcept IrradiationEventCodeableConcept = new CodeableConcept("http://dicom.nema.org/resources/ontology/DCM", "113852", "Irradiation Event"); - - public static readonly ImmutableHashSet IrradiationEvents = ImmutableHashSet.Create( - IrradiationEventXRayData, - CtAcquisition, - RadiopharmaceuticalAdministration); - - public static readonly ImmutableHashSet DoseSummaryReportCodes = ImmutableHashSet.Create( - RadiopharmaceuticalRadiationDoseReport, - XRayRadiationDoseReport); -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/Observation/ObservationDeleteHandler.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/Observation/ObservationDeleteHandler.cs deleted file mode 100644 index 2faeb4ab4a..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/Observation/ObservationDeleteHandler.cs +++ /dev/null @@ -1,59 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Hl7.Fhir.Model; -using Hl7.Fhir.Utility; -using Microsoft.Health.DicomCast.Core.Extensions; -using Microsoft.Health.DicomCast.Core.Features.Fhir; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -public class ObservationDeleteHandler : IObservationDeleteHandler -{ - private readonly IFhirService _fhirService; - - public ObservationDeleteHandler(IFhirService fhirService) - { - _fhirService = EnsureArg.IsNotNull(fhirService, nameof(fhirService)); - } - - public async Task> BuildAsync(FhirTransactionContext context, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(context, nameof(context)); - EnsureArg.IsNotNull(context.ChangeFeedEntry, nameof(context.ChangeFeedEntry)); - - Identifier identifier = IdentifierUtility.CreateIdentifier(context.ChangeFeedEntry.StudyInstanceUid); - List matchingObservations = (await _fhirService.RetrieveObservationsAsync(identifier, cancellationToken)).ToList(); - - // terminate early if no observation found - if (matchingObservations.Count == 0) - { - return null; - } - - var requests = new List(); - foreach (Observation observation in matchingObservations) - { - Bundle.RequestComponent request = new Bundle.RequestComponent() - { - Method = Bundle.HTTPVerb.DELETE, - Url = $"{ResourceType.Observation.GetLiteral()}/{observation.Id}" - }; - - requests.Add(new FhirTransactionRequestEntry( - FhirTransactionRequestMode.Delete, - request, - observation.ToServerResourceId(), - observation)); - } - - return requests; - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/Observation/ObservationParser.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/Observation/ObservationParser.cs deleted file mode 100644 index 5560e95575..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/Observation/ObservationParser.cs +++ /dev/null @@ -1,303 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using EnsureThat; -using FellowOakDicom; -using FellowOakDicom.StructuredReport; -using Hl7.Fhir.Model; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -internal static class ObservationParser -{ - [SuppressMessage("Performance", "CA1859:Use concrete types when possible for improved performance", Justification = "Preserve read-only semantics")] - public static IReadOnlyCollection Parse(DicomDataset dataset, ResourceReference patientReference, ResourceReference imagingStudyReference, Identifier identifier) - { - EnsureArg.IsNotNull(dataset, nameof(dataset)); - EnsureArg.IsNotNull(patientReference, nameof(patientReference)); - EnsureArg.IsNotNull(imagingStudyReference, nameof(imagingStudyReference)); - EnsureArg.IsNotNull(identifier, nameof(identifier)); - - return ParseDicomDataset(dataset, patientReference, imagingStudyReference, identifier).ToList(); - } - - private static IEnumerable ParseDicomDataset(DicomDataset dataset, ResourceReference patientReference, ResourceReference imagingStudyReference, Identifier identifier) - { - if (dataset.TryGetSequence(DicomTag.ConceptNameCodeSequence, out DicomSequence codes) && codes.Items.Count > 0) - { - Observation observation = null; - try - { - var code = createDicomCode(codes); - - if (ObservationConstants.IrradiationEvents.Contains(code) && TryCreateIrradiationEvent(dataset, patientReference, identifier, out Observation irradiationEvent)) - observation = irradiationEvent; - - if (ObservationConstants.DoseSummaryReportCodes.Contains(code)) - observation = CreateDoseSummary(dataset, imagingStudyReference, patientReference, identifier); - } - catch (DicomValidationException) - { - observation = null; //we can safely ignore any validation errors since we are only looking for irradiation and dose summary reports specifically. If the code fails to validate then it will not match either report. - } - - if (observation != null) - yield return observation; - } - - // Recursively iterate through every child in the document checking for nested observations. - // Return the final aggregated list of observations. - if (dataset.TryGetSequence(DicomTag.ContentSequence, out DicomSequence children)) - { - foreach (DicomDataset child in children) - { - foreach (Observation childObservation in ParseDicomDataset(child, patientReference, imagingStudyReference, identifier)) - yield return childObservation; - } - } - } - - // We do not use the built in fo dicom method to create dicom codes as that validates the entire dataset by default and we do not want to do that - private static DicomCodeItem createDicomCode(DicomSequence sequence) - { - string codeValue = sequence.Items[0].GetValueOrDefault(DicomTag.CodeValue, 0, string.Empty); - string scheme = sequence.Items[0].GetValueOrDefault(DicomTag.CodingSchemeDesignator, 0, string.Empty); - string meaning = sequence.Items[0].GetValueOrDefault(DicomTag.CodeMeaning, 0, string.Empty); - - return new DicomCodeItem(codeValue, scheme, meaning); - } - - private static Observation CreateDoseSummary( - DicomDataset dataset, - ResourceReference imagingStudyReference, - ResourceReference patientReference, - Identifier identifier) - { - // Create the observation - var observation = new Observation - { - // Set the code.coding - Code = ObservationConstants.RadiationExposureCodeableConcept, - // Add Patient reference - Subject = patientReference, - Status = ObservationStatus.Preliminary, - }; - // Add ImagingStudy reference - observation.PartOf.Add(imagingStudyReference); - - var report = new DicomStructuredReport(dataset); - - observation.Identifier.Add(identifier); - - // Try to get accession number from report first then tag; ignore if it is not present it is not a required identifier. - string accessionNumber = report.Get(ObservationConstants.AccessionNumber, string.Empty); - if (string.IsNullOrEmpty(accessionNumber)) - { - dataset.TryGetSingleValue(DicomTag.AccessionNumber, out accessionNumber); - } - - if (!string.IsNullOrEmpty(accessionNumber)) - { - var accessionIdentifier = new Identifier - { - Value = accessionNumber, - Type = ObservationConstants.AccessionCodeableConcept - }; - observation.Identifier.Add(accessionIdentifier); - } - - // Add all structured report information - ApplyDicomTransforms(observation, dataset, new Collection() - { - ObservationConstants.DoseRpTotal, - ObservationConstants.AccumulatedAverageGlandularDose, - ObservationConstants.DoseAreaProductTotal, - ObservationConstants.FluoroDoseAreaProductTotal, - ObservationConstants.AcquisitionDoseAreaProductTotal, - ObservationConstants.TotalFluoroTime, - ObservationConstants.TotalNumberOfRadiographicFrames, - ObservationConstants.AdministeredActivity, - ObservationConstants.CtDoseLengthProductTotal, - ObservationConstants.TotalNumberOfIrradiationEvents, - ObservationConstants.MeanCtdIvol, - ObservationConstants.RadiopharmaceuticalAgent, - ObservationConstants.RadiopharmaceuticalVolume, - ObservationConstants.Radionuclide, - ObservationConstants.RouteOfAdministration, - }); - - return observation; - } - - private static bool TryCreateIrradiationEvent(DicomDataset dataset, ResourceReference patientRef, Identifier identifier, out Observation observation) - { - var report = new DicomStructuredReport(dataset); - // create the observation - observation = new Observation - { - Code = ObservationConstants.IrradiationEventCodeableConcept, - Subject = patientRef, - Status = ObservationStatus.Preliminary, - }; - - // try to extract the event UID - DicomUID irradiationEventUidValue = report.Get(ObservationConstants.IrradiationEventUid, null); - if (irradiationEventUidValue == null) - { - observation = default; - return false; - } - - observation.Identifier.Add(identifier); - - DicomCodeItem bodySite = report.Get(ObservationConstants.TargetRegion, null); - if (bodySite != null) - { - observation.BodySite = new CodeableConcept( - GetSystem(bodySite.Scheme), - bodySite.Value, - bodySite.Meaning); - } - - // Extract the necessary information - ApplyDicomTransforms(observation, report.Dataset, new List() - { - ObservationConstants.MeanCtdIvol, - ObservationConstants.Dlp, - ObservationConstants.CtdIwPhantomType - }); - - return true; - } - - private static void ApplyDicomTransforms(Observation observation, - DicomDataset dataset, - IEnumerable reportCodesToParse) - { - var report = new DicomStructuredReport(dataset); - foreach (DicomCodeItem item in reportCodesToParse) - { - if (DicomComponentMutators.TryGetValue(item, - out Action mutator)) - { - mutator(observation, report, item); - } - } - - foreach (DicomContentItem dicomContentItem in report.Children()) - { - ApplyDicomTransforms(observation, dicomContentItem.Dataset, reportCodesToParse); - } - } - - /// - /// Lookup map of DicomCodeItem to Fhir Observation mutator - /// - private static readonly Dictionary> DicomComponentMutators = new() - { - [ObservationConstants.EntranceExposureAtRp] = AddComponentForDicomMeasuredValue, - [ObservationConstants.DoseRpTotal] = AddComponentForDicomMeasuredValue, - [ObservationConstants.AccumulatedAverageGlandularDose] = AddComponentForDicomMeasuredValue, - [ObservationConstants.DoseAreaProductTotal] = AddComponentForDicomMeasuredValue, - [ObservationConstants.FluoroDoseAreaProductTotal] = AddComponentForDicomMeasuredValue, - [ObservationConstants.AcquisitionDoseAreaProductTotal] = AddComponentForDicomMeasuredValue, - [ObservationConstants.TotalFluoroTime] = AddComponentForDicomMeasuredValue, - [ObservationConstants.TotalNumberOfRadiographicFrames] = AddComponentForDicomIntegerValue, - [ObservationConstants.AdministeredActivity] = AddComponentForDicomMeasuredValue, - [ObservationConstants.CtDoseLengthProductTotal] = AddComponentForDicomMeasuredValue, - [ObservationConstants.TotalNumberOfIrradiationEvents] = AddComponentForDicomIntegerValue, - [ObservationConstants.MeanCtdIvol] = AddComponentForDicomMeasuredValue, - [ObservationConstants.RadiopharmaceuticalAgent] = AddComponentForDicomTextValue, - [ObservationConstants.RadiopharmaceuticalVolume] = AddComponentForDicomMeasuredValue, - [ObservationConstants.Radionuclide] = AddComponentForDicomTextValue, - [ObservationConstants.RouteOfAdministration] = AddComponentForDicomCodeValue, - [ObservationConstants.Dlp] = AddComponentForDicomMeasuredValue, - [ObservationConstants.CtdIwPhantomType] = AddComponentForDicomCodeValue, - }; - - private static void AddComponentForDicomMeasuredValue(Observation observation, - DicomStructuredReport report, - DicomCodeItem codeItem) - { - var system = GetSystem(codeItem.Scheme); - var component = new Observation.ComponentComponent - { - Code = new CodeableConcept(system, codeItem.Value, codeItem.Meaning), - }; - DicomMeasuredValue measuredValue = report.Get(codeItem, null); - if (measuredValue != null) - { - component.Value = new Quantity(measuredValue.Value, measuredValue.Code.Value); - observation.Component.Add(component); - } - } - - private static void AddComponentForDicomTextValue(Observation observation, - DicomStructuredReport report, - DicomCodeItem codeItem) - { - string system = GetSystem(codeItem.Scheme); - var component = new Observation.ComponentComponent - { - Code = new CodeableConcept(system, codeItem.Value, codeItem.Meaning), - }; - string value = report.Get(codeItem, string.Empty); - if (!string.IsNullOrEmpty(value)) - { - component.Value = new FhirString(value); - observation.Component.Add(component); - } - } - - - private static void AddComponentForDicomCodeValue(Observation observation, - DicomStructuredReport report, - DicomCodeItem codeItem) - { - string system = GetSystem(codeItem.Scheme); - var component = new Observation.ComponentComponent - { - Code = new CodeableConcept(system, codeItem.Value, codeItem.Meaning), - }; - var value = report.Get(codeItem, null); - if (value != null) - { - component.Value = new CodeableConcept(system, value.Value, value.Meaning); - observation.Component.Add(component); - } - } - - private static void AddComponentForDicomIntegerValue(Observation observation, - DicomStructuredReport report, - DicomCodeItem codeItem) - { - string system = GetSystem(codeItem.Scheme); - var component = new Observation.ComponentComponent - { - Code = new CodeableConcept(system, codeItem.Value, codeItem.Meaning), - }; - int value = report.Get(codeItem, 0); - if (value != 0) - { - component.Value = new Integer(value); - observation.Component.Add(component); - } - } - - private static string GetSystem(string scheme) - { - return scheme switch - { - ObservationConstants.Dcm => ObservationConstants.DcmSystem, - ObservationConstants.Sct => ObservationConstants.SctSystem, - _ => scheme - }; - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/Observation/ObservationPipelineStep.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/Observation/ObservationPipelineStep.cs deleted file mode 100644 index 73c63bf2de..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/Observation/ObservationPipelineStep.cs +++ /dev/null @@ -1,81 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Globalization; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Hl7.Fhir.Utility; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Client.Models; -using Microsoft.Health.DicomCast.Core.Configurations; -using Microsoft.Health.DicomCast.Core.Features.Fhir; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -public class ObservationPipelineStep : FhirTransactionPipelineStepBase -{ - private readonly IObservationDeleteHandler _observationDeleteHandler; - private readonly IObservationUpsertHandler _observationUpsertHandler; - private readonly DicomCastConfiguration _dicomCastConfiguration; - - public ObservationPipelineStep(IObservationDeleteHandler observationDeleteHandler, - IObservationUpsertHandler observationUpsertHandler, - IOptions dicomCastConfiguration, - ILogger logger) - : base(logger) - { - _observationDeleteHandler = EnsureArg.IsNotNull(observationDeleteHandler, nameof(observationDeleteHandler)); - _observationUpsertHandler = EnsureArg.IsNotNull(observationUpsertHandler, nameof(observationUpsertHandler)); - _dicomCastConfiguration = EnsureArg.IsNotNull(dicomCastConfiguration?.Value, nameof(dicomCastConfiguration)); - } - - protected override async Task PrepareRequestImplementationAsync(FhirTransactionContext context, CancellationToken cancellationToken = default) - { - EnsureArg.IsNotNull(context, nameof(context)); - - if (_dicomCastConfiguration.Features.GenerateObservations) - { - ChangeFeedEntry changeFeedEntry = context.ChangeFeedEntry; - context.Request.Observation = changeFeedEntry.Action switch - { - ChangeFeedAction.Create => await _observationUpsertHandler.BuildAsync(context, cancellationToken), - ChangeFeedAction.Delete => await _observationDeleteHandler.BuildAsync(context, cancellationToken), - _ => throw new NotSupportedException( - string.Format( - CultureInfo.InvariantCulture, - DicomCastCoreResource.NotSupportedChangeFeedAction, - changeFeedEntry.Action)) - }; - } - } - - protected override void ProcessResponseImplementation(FhirTransactionContext context) - { - EnsureArg.IsNotNull(context, nameof(context)); - - if (_dicomCastConfiguration.Features.GenerateObservations) - { - if (context.Response?.Observation == null) - { - return; - } - - foreach (FhirTransactionResponseEntry observation in context.Response.Observation) - { - HttpStatusCode statusCode = observation.Response.Annotation(); - - // We are only currently doing POSTs/DELETEs which should result in a 201 or 204 - if (statusCode != HttpStatusCode.Created && statusCode != HttpStatusCode.NoContent) - { - throw new ResourceConflictException(); - } - } - } - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/Observation/ObservationUpsertHandler.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/Observation/ObservationUpsertHandler.cs deleted file mode 100644 index b11fccf101..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/Observation/ObservationUpsertHandler.cs +++ /dev/null @@ -1,71 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Hl7.Fhir.Model; -using Hl7.Fhir.Utility; -using Microsoft.Health.Dicom.Client.Models; -using Microsoft.Health.DicomCast.Core.Features.Fhir; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -public class ObservationUpsertHandler : IObservationUpsertHandler -{ - private readonly IFhirService _fhirService; - - public ObservationUpsertHandler(IFhirService fhirService) - { - _fhirService = EnsureArg.IsNotNull(fhirService, nameof(fhirService)); - } - - public async Task> BuildAsync(FhirTransactionContext context, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(context?.ChangeFeedEntry, nameof(context.ChangeFeedEntry)); - EnsureArg.IsNotNull(context.Request, nameof(context.Request)); - - IResourceId patientId = context.Request.Patient.ResourceId; - IResourceId imagingStudyId = context.Request.ImagingStudy.ResourceId; - ChangeFeedEntry changeFeedEntry = context.ChangeFeedEntry; - - Identifier identifier = IdentifierUtility.CreateIdentifier(changeFeedEntry.StudyInstanceUid); - - IReadOnlyCollection observations = ObservationParser.Parse(changeFeedEntry.Metadata, patientId.ToResourceReference(), imagingStudyId.ToResourceReference(), identifier); - - if (observations.Count == 0) - { - return Enumerable.Empty(); - } - - Identifier imagingStudyIdentifier = imagingStudyId.ToResourceReference().Identifier; - IEnumerable existingDoseSummariesAsync = imagingStudyIdentifier != null - ? await _fhirService - .RetrieveObservationsAsync( - imagingStudyId.ToResourceReference().Identifier, - cancellationToken) - : new List(); - - // TODO: Figure out a way to match existing observations with newly created ones. - - List fhirRequests = new List(); - foreach (var observation in observations) - { - fhirRequests.Add(new FhirTransactionRequestEntry( - FhirTransactionRequestMode.Create, - new Bundle.RequestComponent() - { - Method = Bundle.HTTPVerb.POST, - Url = ResourceType.Observation.GetLiteral() - }, - new ClientResourceId(), - observation)); - } - - return fhirRequests; - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/Patient/IPatientPropertySynchronizer.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/Patient/IPatientPropertySynchronizer.cs deleted file mode 100644 index a6f11db941..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/Patient/IPatientPropertySynchronizer.cs +++ /dev/null @@ -1,23 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using FellowOakDicom; -using Hl7.Fhir.Model; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -/// -/// Provides functionality to synchronize DICOM properties to a specific resource property. -/// -public interface IPatientPropertySynchronizer -{ - /// - /// Synchronizes the DICOM properties to . - /// - /// The DICOM properties. - /// The resource. - /// Flag to determine whether or not the patient being synchronized is new. - void Synchronize(DicomDataset dataset, Patient patient, bool isNewPatient); -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/Patient/IPatientSynchronizer.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/Patient/IPatientSynchronizer.cs deleted file mode 100644 index 915265b3f9..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/Patient/IPatientSynchronizer.cs +++ /dev/null @@ -1,25 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using Hl7.Fhir.Model; -using Task = System.Threading.Tasks.Task; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -/// -/// Provides functionality to synchronize DICOM properties to resource. -/// -public interface IPatientSynchronizer -{ - /// - /// Synchronizes the DICOM properties to . - /// - /// The transaction context. - /// The resource. - /// Flag to determine whether or not the patient being synchronized is new. - /// The cancellation token. - Task SynchronizeAsync(FhirTransactionContext context, Patient patient, bool isNewPatient, CancellationToken cancellationToken = default); -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/Patient/PatientBirthDateSynchronizer.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/Patient/PatientBirthDateSynchronizer.cs deleted file mode 100644 index 853e936f50..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/Patient/PatientBirthDateSynchronizer.cs +++ /dev/null @@ -1,29 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using FellowOakDicom; -using Hl7.Fhir.Model; -using Microsoft.Health.DicomCast.Core.Extensions; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -/// -/// Provides functionality to synchronize DICOM properties to a specific property. -/// -public class PatientBirthDateSynchronizer : IPatientPropertySynchronizer -{ - /// - public void Synchronize(DicomDataset dataset, Patient patient, bool isNewPatient) - { - if (isNewPatient) - { - EnsureArg.IsNotNull(dataset, nameof(dataset)); - EnsureArg.IsNotNull(patient, nameof(patient)); - - patient.BirthDateElement = dataset.GetDatePropertyIfNotDefaultValue(DicomTag.PatientBirthDate); - } - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/Patient/PatientGenderSynchronizer.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/Patient/PatientGenderSynchronizer.cs deleted file mode 100644 index 6561643b1f..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/Patient/PatientGenderSynchronizer.cs +++ /dev/null @@ -1,37 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using FellowOakDicom; -using Hl7.Fhir.Model; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -/// -/// Provides functionality to synchronize DICOM properties to a specific property. -/// -public class PatientGenderSynchronizer : IPatientPropertySynchronizer -{ - private const string EmptyString = ""; - - /// - public void Synchronize(DicomDataset dataset, Patient patient, bool isNewPatient) - { - EnsureArg.IsNotNull(dataset, nameof(dataset)); - EnsureArg.IsNotNull(patient, nameof(patient)); - - if (dataset.TryGetString(DicomTag.PatientSex, out string patientGender)) - { - patient.Gender = patientGender switch - { - "M" => AdministrativeGender.Male, - "F" => AdministrativeGender.Female, - "O" => AdministrativeGender.Other, - EmptyString => null, - _ => throw new InvalidDicomTagValueException(nameof(DicomTag.PatientSex), patientGender), - }; - } - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/Patient/PatientNameSynchronizer.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/Patient/PatientNameSynchronizer.cs deleted file mode 100644 index a42c948728..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/Patient/PatientNameSynchronizer.cs +++ /dev/null @@ -1,86 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Linq; -using EnsureThat; -using FellowOakDicom; -using Hl7.Fhir.Model; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -/// -/// Provides functionality to synchronize DICOM properties to a specific property. -/// -public class PatientNameSynchronizer : IPatientPropertySynchronizer -{ - /// - public void Synchronize(DicomDataset dataset, Patient patient, bool isNewPatient) - { - EnsureArg.IsNotNull(dataset, nameof(dataset)); - EnsureArg.IsNotNull(patient, nameof(patient)); - - // Refer to PS3.5 6.2 and 6.2.1 for parsing logic. - if (dataset.TryGetString(DicomTag.PatientName, out string patientName) && patientName != null) - { - // Find the existing name. - HumanName name = patient.Name.FirstOrDefault(name => name.Use == HumanName.NameUse.Usual); - - if (name == null) - { - name = new HumanName() - { - Use = HumanName.NameUse.Usual, - }; - - patient.Name.Add(name); - } - - string[] parts = patientName.Trim(' ').Split('^'); - - name.Family = parts[0]; - - var combinedGivenNames = new List(); - - if (TryGetNamePart(1, out string[] givenNames)) - { - // Given name. - combinedGivenNames.AddRange(givenNames); - } - - if (TryGetNamePart(2, out string[] middleNames)) - { - // Middle name. - combinedGivenNames.AddRange(middleNames); - } - - if (TryGetNamePart(3, out string[] prefixes)) - { - // Prefix. - name.Prefix = prefixes; - } - - if (TryGetNamePart(4, out string[] suffixes)) - { - // Suffix. - name.Suffix = suffixes; - } - - name.Given = combinedGivenNames; - - bool TryGetNamePart(int index, out string[] nameParts) - { - if (parts.Length > index && !string.IsNullOrWhiteSpace(parts[index])) - { - nameParts = parts[index].Split(' '); - return true; - } - - nameParts = null; - return false; - } - } - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/Patient/PatientPipelineStep.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/Patient/PatientPipelineStep.cs deleted file mode 100644 index 7c027f0894..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/Patient/PatientPipelineStep.cs +++ /dev/null @@ -1,169 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Net; -using System.Net.Http.Headers; -using System.Threading; -using EnsureThat; -using FellowOakDicom; -using Hl7.Fhir.Model; -using Hl7.Fhir.Utility; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Health.DicomCast.Core.Configurations; -using Microsoft.Health.DicomCast.Core.Extensions; -using Microsoft.Health.DicomCast.Core.Features.Fhir; -using Task = System.Threading.Tasks.Task; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -/// -/// Pipeline step for handling . -/// -public class PatientPipelineStep : FhirTransactionPipelineStepBase -{ - private readonly IFhirService _fhirService; - private readonly IPatientSynchronizer _patientSynchronizer; - private readonly string _patientSystemId; - private readonly bool _isIssuerIdUsed; - private readonly ILogger _logger; - - public PatientPipelineStep( - IFhirService fhirService, - IPatientSynchronizer patientSynchronizer, - IOptions patientConfiguration, - ILogger logger) - : base(logger) - { - EnsureArg.IsNotNull(fhirService, nameof(fhirService)); - EnsureArg.IsNotNull(patientSynchronizer, nameof(patientSynchronizer)); - EnsureArg.IsNotNull(patientConfiguration?.Value, nameof(patientConfiguration)); - EnsureArg.IsNotNull(logger, nameof(logger)); - - _fhirService = fhirService; - _patientSynchronizer = patientSynchronizer; - _patientSystemId = patientConfiguration.Value.PatientSystemId; - _isIssuerIdUsed = patientConfiguration.Value.IsIssuerIdUsed; - _logger = logger; - } - - /// - protected override async Task PrepareRequestImplementationAsync(FhirTransactionContext context, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(context, nameof(context)); - - DicomDataset dataset = context.ChangeFeedEntry.Metadata; - - if (dataset == null) - { - return; - } - - if (!dataset.TryGetSingleValue(DicomTag.PatientID, out string patientId)) - { - throw new MissingRequiredDicomTagException(nameof(DicomTag.PatientID)); - } - - // Patient system id is determined based on issuer id boolean - // If issuer id boolean is set to true, patient system id would be set to issuer of patient id (0010,0021) - // Otherwise we will be using the patient system id configured during user provisioning - string patientSystemId = string.Empty; - if (_isIssuerIdUsed) - { - if (dataset.TryGetSingleValue(DicomTag.IssuerOfPatientID, out string systemId)) - { - patientSystemId = systemId; - } - } - else - { - patientSystemId = _patientSystemId; - } - - var patientIdentifier = new Identifier(patientSystemId, patientId); - FhirTransactionRequestMode requestMode = FhirTransactionRequestMode.None; - - Patient existingPatient = await _fhirService.RetrievePatientAsync(patientIdentifier, cancellationToken); - var patient = (Patient)existingPatient?.DeepCopy(); - - if (existingPatient == null) - { - patient = new Patient(); - - patient.Identifier.Add(patientIdentifier); - - requestMode = FhirTransactionRequestMode.Create; - } - - await _patientSynchronizer.SynchronizeAsync(context, patient, requestMode.Equals(FhirTransactionRequestMode.Create), cancellationToken); - - if (requestMode == FhirTransactionRequestMode.None && - !existingPatient.IsExactly(patient)) - { - requestMode = FhirTransactionRequestMode.Update; - } - - Bundle.RequestComponent request = requestMode switch - { - FhirTransactionRequestMode.Create => GenerateCreateRequest(patientIdentifier), - FhirTransactionRequestMode.Update => GenerateUpdateRequest(patient), - _ => null - }; - - IResourceId resourceId = requestMode switch - { - FhirTransactionRequestMode.Create => new ClientResourceId(), - _ => existingPatient.ToServerResourceId(), - }; - - context.Request.Patient = new FhirTransactionRequestEntry( - requestMode, - request, - resourceId, - patient); - } - - /// - protected override void ProcessResponseImplementation(FhirTransactionContext context) - { - EnsureArg.IsNotNull(context, nameof(context)); - - // If the Patient does not exist, we will use conditional create to create the resource - // to avoid duplicated resource being created. However, if the resource with the identifier - // was created externally between the retrieve and create, conditional create will return 200 - // and might not contain the changes so we will need to try again. - if (context.Request.Patient?.RequestMode == FhirTransactionRequestMode.Create) - { - FhirTransactionResponseEntry patient = context.Response.Patient; - - HttpStatusCode statusCode = patient.Response.Annotation(); - - if (statusCode == HttpStatusCode.OK) - { - throw new ResourceConflictException(); - } - } - } - - private static Bundle.RequestComponent GenerateCreateRequest(Identifier patientIdentifier) - { - return new Bundle.RequestComponent() - { - Method = Bundle.HTTPVerb.POST, - IfNoneExist = patientIdentifier.ToSearchQueryParameter(), - Url = ResourceType.Patient.GetLiteral(), - }; - } - - private static Bundle.RequestComponent GenerateUpdateRequest(Patient patient) - { - return new Bundle.RequestComponent() - { - Method = Bundle.HTTPVerb.PUT, - IfMatch = new EntityTagHeaderValue($"\"{patient.Meta.VersionId}\"", true).ToString(), - Url = $"{ResourceType.Patient.GetLiteral()}/{patient.Id}", - }; - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/Patient/PatientSynchronizer.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/Patient/PatientSynchronizer.cs deleted file mode 100644 index a263494dd9..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/Patient/PatientSynchronizer.cs +++ /dev/null @@ -1,73 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Threading; -using EnsureThat; -using FellowOakDicom; -using Hl7.Fhir.Model; -using Microsoft.Extensions.Options; -using Microsoft.Health.DicomCast.Core.Configurations; -using Microsoft.Health.DicomCast.Core.Exceptions; -using Microsoft.Health.DicomCast.Core.Features.ExceptionStorage; -using Task = System.Threading.Tasks.Task; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -/// -/// Provides functionality to synchronize DICOM properties to resource. -/// -public class PatientSynchronizer : IPatientSynchronizer -{ - private readonly IEnumerable _patientPropertySynchronizers; - private readonly DicomCastConfiguration _dicomCastconfiguration; - private readonly IExceptionStore _exceptionStore; - - public PatientSynchronizer( - IEnumerable patientPropertySynchronizers, - IOptions dicomCastConfiguration, - IExceptionStore exceptionStore) - { - EnsureArg.IsNotNull(patientPropertySynchronizers, nameof(patientPropertySynchronizers)); - EnsureArg.IsNotNull(dicomCastConfiguration, nameof(dicomCastConfiguration)); - EnsureArg.IsNotNull(exceptionStore, nameof(exceptionStore)); - - _patientPropertySynchronizers = patientPropertySynchronizers; - _dicomCastconfiguration = dicomCastConfiguration.Value; - _exceptionStore = exceptionStore; - } - - /// - public async Task SynchronizeAsync(FhirTransactionContext context, Patient patient, bool isNewPatient, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(context, nameof(context)); - EnsureArg.IsNotNull(patient, nameof(patient)); - - DicomDataset dataset = context.ChangeFeedEntry.Metadata; - - foreach (IPatientPropertySynchronizer patientPropertySynchronizer in _patientPropertySynchronizers) - { - try - { - patientPropertySynchronizer.Synchronize(dataset, patient, isNewPatient); - } - catch (DicomTagException ex) - { - if (!_dicomCastconfiguration.Features.EnforceValidationOfTagValues) - { - await _exceptionStore.WriteExceptionAsync( - context.ChangeFeedEntry, - ex, - ErrorType.DicomValidationError, - cancellationToken); - } - else - { - throw; - } - } - } - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ResourceId/ClientResourceId.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ResourceId/ClientResourceId.cs deleted file mode 100644 index a39d191224..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ResourceId/ClientResourceId.cs +++ /dev/null @@ -1,64 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using Hl7.Fhir.Model; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -/// -/// Represents a client generated resource identifier. -/// -public class ClientResourceId : IResourceId, IEquatable -{ - private ResourceReference _resourceReference; - - public ClientResourceId() - { - Id = $"urn:uuid:{Guid.NewGuid()}"; - } - - /// - /// Gets the client generated resource identifier. - /// - public string Id { get; } - - /// - public ResourceReference ToResourceReference() - { - _resourceReference ??= new ResourceReference(Id); - - return _resourceReference; - } - - public override int GetHashCode() - { - return HashCode.Combine(Id); - } - - public override bool Equals(object obj) - { - return Equals(obj as ClientResourceId); - } - - public bool Equals(ClientResourceId other) - { - if (other == null) - { - return false; - } - else if (ReferenceEquals(this, other)) - { - return true; - } - else - { - return string.Equals(Id, other.Id, StringComparison.Ordinal); - } - } - - public override string ToString() - => Id; -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ResourceId/IResourceId.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ResourceId/IResourceId.cs deleted file mode 100644 index 64d6d4e759..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ResourceId/IResourceId.cs +++ /dev/null @@ -1,20 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Hl7.Fhir.Model; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -/// -/// Represents a resource identifier. -/// -public interface IResourceId -{ - /// - /// Converts to resource identifier to . - /// - /// An instance of . - ResourceReference ToResourceReference(); -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ResourceId/ServerResourceId.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ResourceId/ServerResourceId.cs deleted file mode 100644 index 512f9dcb75..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/FhirTransaction/ResourceId/ServerResourceId.cs +++ /dev/null @@ -1,101 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Globalization; -using EnsureThat; -using Hl7.Fhir.Model; -using Hl7.Fhir.Utility; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; - -/// -/// Represents server generated resource identifier. -/// -public class ServerResourceId : IResourceId, IEquatable -{ - private ResourceReference _resourceReference; - - public ServerResourceId(ResourceType resourceType, string resourceId) - : this(EnumUtility.GetLiteral(resourceType), resourceId) - { - } - - public ServerResourceId(string typeName, string resourceId) - { - EnsureArg.IsNotNullOrWhiteSpace(typeName, nameof(typeName)); - EnsureArg.IsNotNullOrWhiteSpace(resourceId, nameof(resourceId)); - - TypeName = typeName; - ResourceId = resourceId; - } - - /// - /// Gets the type name. - /// - public string TypeName { get; } - - /// - /// Gets the resource type. - /// - [Obsolete("Please use TypeName instead.")] - public ResourceType ResourceType - { - get - { - ResourceType? resourceType = ModelInfo.FhirTypeNameToResourceType(TypeName); - if (resourceType == null) - { - throw new InvalidOperationException( - string.Format(CultureInfo.InvariantCulture, DicomCastCoreResource.UnknownResourceType, TypeName)); - } - - return resourceType.GetValueOrDefault(); - } - } - - /// - /// Gets the server generated resource id. - /// - public string ResourceId { get; } - - /// - public ResourceReference ToResourceReference() - { - _resourceReference ??= new ResourceReference(TypeName + "/" + ResourceId); - - return _resourceReference; - } - - public override int GetHashCode() - { - return HashCode.Combine(TypeName, ResourceId); - } - - public override bool Equals(object obj) - { - return Equals(obj as ServerResourceId); - } - - public bool Equals(ServerResourceId other) - { - if (other == null) - { - return false; - } - else if (ReferenceEquals(this, other)) - { - return true; - } - else - { - return string.Equals(TypeName, other.TypeName, StringComparison.Ordinal) && - string.Equals(ResourceId, other.ResourceId, StringComparison.Ordinal); - } - } - - public override string ToString() - => ToResourceReference().Reference; -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/IChangeFeedProcessor.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/IChangeFeedProcessor.cs deleted file mode 100644 index 7a9bd3d27e..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/IChangeFeedProcessor.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker; - -/// -/// Provides functionality to process the change feed. -/// -public interface IChangeFeedProcessor -{ - /// - /// Asynchronously processes the change feed. - /// - /// The delay between polls during catchup phase. - /// The cancellation token. - /// A task representing the asynchronous processing operation. - Task ProcessAsync(TimeSpan pollIntervalDuringCatchup, CancellationToken cancellationToken); -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/IDicomCastWorker.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/IDicomCastWorker.cs deleted file mode 100644 index 8eb2f10d51..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/Worker/IDicomCastWorker.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.Health.DicomCast.Core.Features.Worker; - -/// -/// The worker for DicomCast. -/// -public interface IDicomCastWorker -{ - /// - /// Asynchronously executes the worker. - /// - /// The cancellation token. - /// A task that represents asynchronous worker execution. - Task ExecuteAsync(CancellationToken cancellationToken); -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Microsoft.Health.DicomCast.Core.csproj b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Microsoft.Health.DicomCast.Core.csproj deleted file mode 100644 index e07526821c..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Microsoft.Health.DicomCast.Core.csproj +++ /dev/null @@ -1,51 +0,0 @@ - - - - Common primitives and utilities used by Microsoft's DICOM Cast APIs. - $(LatestVersion) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - True - True - DicomCastCoreResource.resx - - - - - - ResXFileCodeGenerator - DicomCastCoreResource.Designer.cs - - - - diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Modules/DicomModule.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Modules/DicomModule.cs deleted file mode 100644 index 2dfcccedde..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Modules/DicomModule.cs +++ /dev/null @@ -1,29 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Health.Extensions.DependencyInjection; - -namespace Microsoft.Health.DicomCast.Core.Modules; - -public class DicomModule : IStartupModule -{ - private readonly IConfiguration _configuration; - - public DicomModule(IConfiguration configuration) - { - EnsureArg.IsNotNull(configuration, nameof(configuration)); - - _configuration = configuration; - } - - public void Load(IServiceCollection services) - { - EnsureArg.IsNotNull(services, nameof(services)); - services.AddDicomModule(_configuration); - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Modules/DicomModulesExtensions.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Modules/DicomModulesExtensions.cs deleted file mode 100644 index efdbb32b05..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Modules/DicomModulesExtensions.cs +++ /dev/null @@ -1,56 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using EnsureThat; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Microsoft.Health.Client.Extensions; -using Microsoft.Health.Dicom.Client; -using Microsoft.Health.DicomCast.Core.Configurations; -using Microsoft.Health.DicomCast.Core.Features.DicomWeb.Service; -using Microsoft.Health.Extensions.DependencyInjection; -using Polly; -using Polly.Extensions.Http; -using Microsoft.Health.Client.Authentication; - -namespace Microsoft.Extensions.DependencyInjection; - -public static class DicomModulesExtensions -{ - private const string DicomWebConfigurationSectionName = "DicomWeb"; - - public static IServiceCollection AddDicomModule(this IServiceCollection services, IConfiguration configuration) - { - EnsureArg.IsNotNull(services, nameof(services)); - EnsureArg.IsNotNull(configuration, nameof(configuration)); - - IConfigurationSection dicomWebConfigurationSection = configuration.GetSection(DicomWebConfigurationSectionName); - services.AddOptions().Bind(dicomWebConfigurationSection); - - // Allow retries to occur catch 30 second outages - var retryPolicy = HttpPolicyExtensions - .HandleTransientHttpError() // HttpRequestException, 5XX and 408 - .WaitAndRetryAsync(8, retryAttempt => retryAttempt <= 3 ? TimeSpan.FromSeconds(retryAttempt) : TimeSpan.FromSeconds(5)); - - services.AddHttpClient( - (httpClient, sp) => - { - DicomWebConfiguration config = sp.GetRequiredService>().Value; - httpClient.BaseAddress = config.PrivateEndpoint == null ? config.Endpoint : config.PrivateEndpoint; - return new DicomWebClient(httpClient, DicomApiVersions.V1); - }) - .AddPolicyHandler(retryPolicy) - .AddAuthenticationHandler(dicomWebConfigurationSection.GetSection(AuthenticationOptions.SectionName)); - - services.Add() - .Singleton() - .AsSelf() - .AsImplementedInterfaces(); - - return services; - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Modules/FhirModule.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Modules/FhirModule.cs deleted file mode 100644 index 0fd3f093fc..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Modules/FhirModule.cs +++ /dev/null @@ -1,83 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Health.Client.Authentication; -using Microsoft.Health.Client.Extensions; -using Microsoft.Health.DicomCast.Core.Configurations; -using Microsoft.Health.DicomCast.Core.Features.Fhir; -using Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; -using Microsoft.Health.Extensions.DependencyInjection; -using FhirClient = Microsoft.Health.Fhir.Client.FhirClient; -using IFhirClient = Microsoft.Health.Fhir.Client.IFhirClient; - -namespace Microsoft.Health.DicomCast.Core.Modules; - -public class FhirModule : IStartupModule -{ - private const string FhirConfigurationSectionName = "Fhir"; - - private readonly IConfiguration _configuration; - - public FhirModule(IConfiguration configuration) - { - EnsureArg.IsNotNull(configuration, nameof(configuration)); - - _configuration = configuration; - } - - public void Load(IServiceCollection services) - { - EnsureArg.IsNotNull(services, nameof(services)); - - var fhirConfiguration = new FhirConfiguration(); - IConfigurationSection fhirConfigurationSection = _configuration.GetSection(FhirConfigurationSectionName); - - fhirConfigurationSection.Bind(fhirConfiguration); - - services.AddHttpClient(sp => - { - sp.BaseAddress = fhirConfiguration.Endpoint; - }) - .AddAuthenticationHandler(fhirConfigurationSection.GetSection(AuthenticationOptions.SectionName)); - - services.Add() - .Singleton() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Singleton() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Singleton() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Singleton() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Singleton() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Singleton() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Singleton() - .AsSelf() - .AsImplementedInterfaces(); - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Modules/WorkerModule.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Modules/WorkerModule.cs deleted file mode 100644 index 6ee7c0d290..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Modules/WorkerModule.cs +++ /dev/null @@ -1,155 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using EnsureThat; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Health.DicomCast.Core.Configurations; -using Microsoft.Health.DicomCast.Core.Extensions; -using Microsoft.Health.DicomCast.Core.Features.State; -using Microsoft.Health.DicomCast.Core.Features.Worker; -using Microsoft.Health.DicomCast.Core.Features.Worker.FhirTransaction; -using Microsoft.Health.Extensions.DependencyInjection; - -namespace Microsoft.Health.DicomCast.Core.Modules; - -public class WorkerModule : IStartupModule -{ - private const string DicomCastWorkerConfigurationSectionName = "DicomCastWorker"; - private const string DicomValidationConfigurationSectionName = "DicomCast"; - private const string RetryConfigurationSectionName = "RetryConfiguration"; - private const string PatientConfigurationSectionName = "Patient"; - - private readonly IConfiguration _configuration; - - public WorkerModule(IConfiguration configuration) - { - EnsureArg.IsNotNull(configuration, nameof(configuration)); - - _configuration = configuration; - } - - public void Load(IServiceCollection services) - { - EnsureArg.IsNotNull(services, nameof(services)); - - DicomCastWorkerConfiguration dicomCastWorkerConfiguration = services.Configure( - _configuration, - DicomCastWorkerConfigurationSectionName); - - DicomCastConfiguration dicomValidationConfiguration = services.Configure( - _configuration, - DicomValidationConfigurationSectionName); - - RetryConfiguration retryConfiguration = services.Configure( - _configuration, - RetryConfigurationSectionName); - - PatientConfiguration patientConfiguration = services.Configure( - _configuration, - PatientConfigurationSectionName); - - services.Add() - .Singleton() - .AsSelf() - .AsImplementedInterfaces(); - - RegisterPipeline(services); - - services.Add() - .Singleton() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Singleton() - .AsSelf() - .AsImplementedInterfaces(); - } - - private static void RegisterPipeline(IServiceCollection services) - { - services.Add() - .Transient() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add>(sp => () => sp.GetRequiredService()) - .Transient() - .AsSelf(); - - RegisterPipelineSteps(services); - - services.Add() - .Singleton() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Singleton() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Singleton() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Singleton() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Singleton() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Singleton() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Singleton() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Singleton() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Singleton() - .AsSelf() - .AsImplementedInterfaces(); - } - - private static void RegisterPipelineSteps(IServiceCollection services) - { - // The order matters for the following pipeline steps. - services.Add() - .Singleton() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Singleton() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Singleton() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Singleton() - .AsSelf() - .AsImplementedInterfaces(); - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Properties/AssemblyInfo.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Properties/AssemblyInfo.cs deleted file mode 100644 index ea27f59801..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,12 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Resources; -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("Microsoft.Health.DicomCast.Core.UnitTests")] -[assembly: NeutralResourcesLanguage("en-us")] -[assembly: CLSCompliant(false)] diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Hosting/DicomCastBackgroundService.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Hosting/DicomCastBackgroundService.cs deleted file mode 100644 index cd0d1b1166..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Hosting/DicomCastBackgroundService.cs +++ /dev/null @@ -1,27 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.Extensions.Hosting; -using Microsoft.Health.DicomCast.Core.Features.Worker; - -namespace Microsoft.Health.DicomCast.Hosting; - -public class DicomCastBackgroundService : BackgroundService -{ - private readonly IDicomCastWorker _dicomCastWorker; - - public DicomCastBackgroundService(IDicomCastWorker dicomCastWorker) - { - EnsureArg.IsNotNull(dicomCastWorker, nameof(dicomCastWorker)); - - _dicomCastWorker = dicomCastWorker; - } - - protected override Task ExecuteAsync(CancellationToken stoppingToken) - => _dicomCastWorker.ExecuteAsync(stoppingToken); -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Hosting/Dockerfile b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Hosting/Dockerfile deleted file mode 100644 index 6af68a0f8f..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Hosting/Dockerfile +++ /dev/null @@ -1,23 +0,0 @@ -# Define the "runtime" image which will run DICOMcast -FROM mcr.microsoft.com/dotnet/aspnet:8.0.6-alpine3.18-amd64@sha256:5826c371d350e769022b3c8736a371fa49942d19aba5136151c584ae7cfc8248 AS runtime -USER $APP_UID - -# Copy the DICOMcast project and build it -FROM mcr.microsoft.com/dotnet/sdk:8.0.301-alpine3.18-amd64@sha256:5ccd7acc1ff31f2a0377bcbc50bd0553c28d65cd4f5ec4366e68966aea60bf2f AS build -ARG BUILD_CONFIGURATION=Release -ARG CONTINUOUS_INTEGRATION_BUILD=false -WORKDIR /dicom-server -COPY . . -WORKDIR /dicom-server/converter/dicom-cast/src/Microsoft.Health.DicomCast.Hosting/ -RUN dotnet build "Microsoft.Health.DicomCast.Hosting.csproj" -c $BUILD_CONFIGURATION -p:ContinuousIntegrationBuild=$CONTINUOUS_INTEGRATION_BUILD -warnaserror - -# Publish the DICOM Server from the build -FROM build as publish -ARG BUILD_CONFIGURATION=Release -RUN dotnet publish "Microsoft.Health.DicomCast.Hosting.csproj" -c $BUILD_CONFIGURATION --no-build -o /app/publish - -# Copy the published application -FROM runtime AS dicom-cast -WORKDIR /app -COPY --from=publish /app/publish . -ENTRYPOINT ["dotnet", "Microsoft.Health.DicomCast.Hosting.dll"] diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Hosting/Microsoft.Health.DicomCast.Hosting.csproj b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Hosting/Microsoft.Health.DicomCast.Hosting.csproj deleted file mode 100644 index aefe2e1c83..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Hosting/Microsoft.Health.DicomCast.Hosting.csproj +++ /dev/null @@ -1,27 +0,0 @@ - - - - $(NoWarn);NU5104 - $(LatestVersion) - - - - - - - - - - - - - - - - - - - - - - diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Hosting/Program.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Hosting/Program.cs deleted file mode 100644 index 6b63f3874e..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Hosting/Program.cs +++ /dev/null @@ -1,95 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using Azure.Extensions.AspNetCore.Configuration.Secrets; -using Azure.Identity; -using Azure.Monitor.OpenTelemetry.Exporter; -using Azure.Security.KeyVault.Secrets; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Health.DicomCast.Core.Features.Worker; -using Microsoft.Health.DicomCast.Core.Modules; -using Microsoft.Health.DicomCast.TableStorage; -using Microsoft.Health.Extensions.DependencyInjection; -using OpenTelemetry; -using OpenTelemetry.Metrics; - -namespace Microsoft.Health.DicomCast.Hosting; - -public static class Program -{ - public static void Main(string[] args) - { - IHost host = Host.CreateDefaultBuilder(args) - .ConfigureAppConfiguration((hostContext, builder) => - { - IConfiguration builtConfig = builder.Build(); - - // TODO: Use Azure SDK directly for settings - string keyVaultEndpoint = builtConfig["KeyVault:Endpoint"]; - if (!string.IsNullOrEmpty(keyVaultEndpoint)) - { - builder.AddAzureKeyVault( - new SecretClient(new Uri(keyVaultEndpoint), new DefaultAzureCredential()), - new AzureKeyVaultConfigurationOptions()); - } - }) - .ConfigureServices((hostContext, services) => - { - IConfiguration configuration = hostContext.Configuration; - - services.RegisterAssemblyModules(typeof(WorkerModule).Assembly, configuration); - - services.AddTableStorageDataStore(configuration); - - services.AddHostedService(); - - AddTelemetry(services, configuration); - }) - .Build(); - - host.Run(); - } - - private static void AddTelemetry(IServiceCollection services, IConfiguration configuration) - { - string instrumentationKey = configuration["ApplicationInsights:InstrumentationKey"]; - - if (!string.IsNullOrWhiteSpace(instrumentationKey)) - { - var connectionString = $"InstrumentationKey={instrumentationKey}"; - AddApplicationInsightsTelemetry(services, connectionString); - AddOpenTelemetryMetrics(services, connectionString); - } - } - - /// - /// Adds Open Telemetry exporter for Azure Monitor. - /// - private static void AddOpenTelemetryMetrics(IServiceCollection services, string connectionString) - { - services.AddSingleton(); - services.AddSingleton(Sdk.CreateMeterProviderBuilder() - .AddMeter("Microsoft.Health.DicomCast") - .AddAzureMonitorMetricExporter(o => o.ConnectionString = connectionString) - .Build()); - } - - /// - /// Adds ApplicationInsights for logging. - /// - private static void AddApplicationInsightsTelemetry(IServiceCollection services, string connectionString) - { - services.AddApplicationInsightsTelemetryWorkerService(aiOptions => aiOptions.ConnectionString = connectionString); - services.AddLogging( - loggingBuilder => loggingBuilder.AddApplicationInsights( - telemetryConfig => telemetryConfig.ConnectionString = connectionString, - aiLoggerOptions => { } - )); - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Hosting/Properties/AssemblyInfo.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Hosting/Properties/AssemblyInfo.cs deleted file mode 100644 index 299f677eb6..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Hosting/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,8 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; - -[assembly: CLSCompliant(false)] diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Hosting/Properties/launchSettings.json b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Hosting/Properties/launchSettings.json deleted file mode 100644 index ded0f9e5cb..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Hosting/Properties/launchSettings.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "profiles": { - "Microsoft.Health.DicomCast.Hosting": { - "commandName": "Project", - "environmentVariables": { - "DOTNET_ENVIRONMENT": "Development" - } - }, - "Auth Enabled": { - "commandName": "Project", - "environmentVariables": { - "DOTNET_ENVIRONMENT": "Development", - "DicomWeb:Authentication:Enabled": "true", - "DicomWeb:Authentication:AuthenticationType": "OAuth2ClientCredential", - "DicomWeb:Authentication:OAuth2ClientCredential:TokenUri": "https://localhost:63838/connect/token", - "DicomWeb:Authentication:OAuth2ClientCredential:Resource": "health-api", - "DicomWeb:Authentication:OAuth2ClientCredential:Scope": "health-api", - "DicomWeb:Authentication:OAuth2ClientCredential:ClientId": "globalAdminServicePrincipal", - "DicomWeb:Authentication:OAuth2ClientCredential:ClientSecret": "globalAdminServicePrincipal", - "Fhir:Authentication:Enabled": "true", - "Fhir:Authentication:AuthenticationType": "OAuth2ClientCredential", - "Fhir:Authentication:OAuth2ClientCredential:TokenUri": "https://localhost:44348/connect/token", - "Fhir:Authentication:OAuth2ClientCredential:Resource": "fhir-api", - "Fhir:Authentication:OAuth2ClientCredential:Scope": "fhir-api", - "Fhir:Authentication:OAuth2ClientCredential:ClientId": "globalAdminServicePrincipal", - "Fhir:Authentication:OAuth2ClientCredential:ClientSecret": "globalAdminServicePrincipal" - } - } - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Hosting/appsettings.Development.json b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Hosting/appsettings.Development.json deleted file mode 100644 index dc7d873620..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Hosting/appsettings.Development.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "Logging": { - "Console": { - "IncludeScopes": true - }, - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information", - "Microsoft.Health": "Information" - } - }, - "DicomWeb": { - "Endpoint": "https://localhost:63838", - "Authentication": { - "Enabled": false - } - }, - "Fhir": { - "Endpoint": "https://localhost:44348", - "Authentication": { - "Enabled": false - } - }, - "DicomCastWorker": { - "PollInterval": "00:00:05" - }, - "Patient": { - "PatientSystemId": "patientSystemId", - "IsIssuerIdUsed": false - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Hosting/appsettings.json b/converter/dicom-cast/src/Microsoft.Health.DicomCast.Hosting/appsettings.json deleted file mode 100644 index 19987ead47..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.Hosting/appsettings.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Health": "Information", - "Microsoft.Hosting.Lifetime": "Information", - "System": "Warning" - }, - "ApplicationInsights": { - "LogLevel": { - "Default": "Information", - "Microsoft.Health": "Information", - "Microsoft": "Warning", - "System": "Warning" - } - } - }, - "DicomWeb": { - "Endpoint": "", - "Authentication": { - "Enabled": false - } - }, - "Fhir": { - "Endpoint": "", - "Authentication": { - "Enabled": false - } - }, - "DicomCastWorker": { - "PollInterval": "00:01:00", - "PollIntervalDuringCatchup": "00:00:00" - }, - "DicomCast": { - "Features": { - "EnforceValidationOfTagValues": false, - "GenerateObservations": true, - "IgnoreJsonParsingErrors": false - } - }, - "RetryConfiguration": { - "TotalRetryDuration": "00:10:00" - }, - "ApplicationInsights": { - "InstrumentationKey": "12345" - }, - "TableStore": { - "TableNamePrefix": "", - "ConnectionString": null - }, - "Patient": { - "PatientSystemId": "", - "IsIssuerIdUsed": false - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage.UnitTests/Features/Health/TableHealthCheckTests.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage.UnitTests/Features/Health/TableHealthCheckTests.cs deleted file mode 100644 index 4fcd6dae45..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage.UnitTests/Features/Health/TableHealthCheckTests.cs +++ /dev/null @@ -1,42 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Net.Http; -using System.Threading.Tasks; -using Microsoft.Extensions.Diagnostics.HealthChecks; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Health.DicomCast.TableStorage.Features.Health; -using Microsoft.Health.DicomCast.TableStorage.Features.Storage; -using NSubstitute; -using NSubstitute.ExceptionExtensions; -using Xunit; - -namespace Microsoft.Health.DicomCast.TableStorage.UnitTests.Features.Health; - -public class TableHealthCheckTests -{ - private readonly ITableClientTestProvider _testProvider = Substitute.For(); - - private readonly TableHealthCheck _healthCheck; - - public TableHealthCheckTests() - { - _healthCheck = new TableHealthCheck(_testProvider, NullLogger.Instance); - } - - [Fact] - public async Task GivenTableDataStoreIsAvailable_WhenTableIsChecked_ThenHealthyStateShouldBeReturned() - { - HealthCheckResult result = await _healthCheck.CheckHealthAsync(new HealthCheckContext()); - Assert.Equal(HealthStatus.Healthy, result.Status); - } - - [Fact] - public async Task GivenTableDataStoreIsNotAvailable_WhenHealthIsChecked_ThenUnhealthyStateShouldBeReturned() - { - _testProvider.PerformTestAsync(default).ThrowsForAnyArgs(); - await Assert.ThrowsAsync(() => _healthCheck.CheckHealthAsync(new HealthCheckContext())); - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage.UnitTests/Features/Storage/TableExceptionStoreTests.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage.UnitTests/Features/Storage/TableExceptionStoreTests.cs deleted file mode 100644 index 738ef8079b..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage.UnitTests/Features/Storage/TableExceptionStoreTests.cs +++ /dev/null @@ -1,51 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Azure.Data.Tables; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using Microsoft.Health.DicomCast.Core.UnitTests; -using Microsoft.Health.DicomCast.Core.UnitTests.Features.Worker.FhirTransaction; -using Microsoft.Health.DicomCast.TableStorage.Configs; -using Microsoft.Health.DicomCast.TableStorage.Features.Storage; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.DicomCast.TableStorage.UnitTests.Features.Storage; - -public class TableExceptionStoreTests -{ - private readonly TableExceptionStore _tableExceptionStore; - private readonly TableDataStoreConfiguration _tableStorageConfiguration = new TableDataStoreConfiguration(); - - public TableExceptionStoreTests() - { - Dictionary _tableNames = new Dictionary(); - _tableNames.Add("FhirFailToStoreExceptionTable", "FhirFailToStoreExceptionTable"); - - _tableStorageConfiguration.TableNamePrefix = ""; - - - - TableServiceClientProvider tableServiceClientProvider = new TableServiceClientProvider - (Substitute.For(), Substitute.For(), Options.Create(_tableStorageConfiguration), NullLogger.Instance); - - - - _tableExceptionStore = new TableExceptionStore(tableServiceClientProvider, NullLogger.Instance); - } - - [Fact] - public async Task GivenTableExceptionSToreWithNoDicomCastName_WhenExceptionsAreThrown_AreStoredInTablesSuccessfully() - { - - await _tableExceptionStore.WriteExceptionAsync(ChangeFeedGenerator.Generate(1, metadata: FhirTransactionContextBuilder.CreateDicomDataset()), new Exception("new Exception"), Core.Features.ExceptionStorage.ErrorType.FhirError, CancellationToken.None); - } - -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage.UnitTests/Microsoft.Health.DicomCast.TableStorage.UnitTests.csproj b/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage.UnitTests/Microsoft.Health.DicomCast.TableStorage.UnitTests.csproj deleted file mode 100644 index 21635f7a37..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage.UnitTests/Microsoft.Health.DicomCast.TableStorage.UnitTests.csproj +++ /dev/null @@ -1,23 +0,0 @@ - - - - $(LatestVersion) - - - - - - - - - - - - - - - - - - - diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Configs/TableDataStoreConfiguration.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Configs/TableDataStoreConfiguration.cs deleted file mode 100644 index 3e9abfac0f..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Configs/TableDataStoreConfiguration.cs +++ /dev/null @@ -1,31 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; - -namespace Microsoft.Health.DicomCast.TableStorage.Configs; - -public class TableDataStoreConfiguration -{ - /// - /// The storage table connection string to use. Setting this assumes the use of an account key. - /// - public string ConnectionString { get; set; } - - /// - /// The endpoint of the table storage account. Setting this assumes the use of a managed identity to communicate. - /// - public Uri EndpointUri { get; set; } - - /// - /// Optional parameter to use to specify the clientId of the managed identity to use. - /// - public string ClientId { get; set; } - - /// - ///The prefix to the table name. Default value is empty. - /// - public string TableNamePrefix { get; set; } = string.Empty; -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Constants.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Constants.cs deleted file mode 100644 index 090a0b902c..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Constants.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; - -namespace Microsoft.Health.DicomCast.TableStorage; - -internal static class Constants -{ - public const string FhirExceptionTableName = "FhirFailToStoreExceptionTable"; - - public const string DicomExceptionTableName = "DicomFailToStoreExceptionTable"; - - public const string DicomValidationTableName = "InvalidDicomTagExceptionTable"; - - public const string TransientFailureTableName = "TransientFailureExceptionTable"; - - public const string TransientRetryTableName = "TransientRetryExceptionTable"; - - public const string SyncStateTableName = "SyncStateTable"; - - public const string SyncStatePartitionKey = "SyncStatePartitionKey"; - - public const string SyncStateRowKey = "SyncStateRowKey"; - - // List of all the table name suffixes that need to be initialized - public static readonly IReadOnlyList AllTables = new List { SyncStateTableName, FhirExceptionTableName, DicomExceptionTableName, TransientFailureTableName, DicomValidationTableName, TransientRetryTableName }; -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Features/Health/TableHealthCheck.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Features/Health/TableHealthCheck.cs deleted file mode 100644 index 42405c1ac8..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Features/Health/TableHealthCheck.cs +++ /dev/null @@ -1,46 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.Extensions.Diagnostics.HealthChecks; -using Microsoft.Extensions.Logging; -using Microsoft.Health.DicomCast.TableStorage.Features.Storage; - -namespace Microsoft.Health.DicomCast.TableStorage.Features.Health; - -public class TableHealthCheck : IHealthCheck -{ - private readonly ITableClientTestProvider _testProvider; - private readonly ILogger _logger; - - public TableHealthCheck( - ITableClientTestProvider testProvider, - ILogger logger) - { - EnsureArg.IsNotNull(testProvider, nameof(testProvider)); - EnsureArg.IsNotNull(logger, nameof(logger)); - - _testProvider = testProvider; - _logger = logger; - } - - public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) - { - try - { - await _testProvider.PerformTestAsync(cancellationToken); - - return HealthCheckResult.Healthy("Successfully connected to the table data store."); - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to connect to the table data store."); - throw; - } - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Features/Storage/ITableClientTestProvider.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Features/Storage/ITableClientTestProvider.cs deleted file mode 100644 index e1ad9dc9eb..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Features/Storage/ITableClientTestProvider.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.Health.DicomCast.TableStorage.Features.Storage; - -public interface ITableClientTestProvider -{ - /// - /// Check to make sure Table Storage is set up and is working properly - /// - /// Cancellation Token - /// A . - Task PerformTestAsync(CancellationToken cancellationToken = default); -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Features/Storage/ITableServiceClientInitializer.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Features/Storage/ITableServiceClientInitializer.cs deleted file mode 100644 index 4b062636f7..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Features/Storage/ITableServiceClientInitializer.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Threading.Tasks; -using Azure.Data.Tables; - -namespace Microsoft.Health.DicomCast.TableStorage.Features.Storage; - -/// -/// Provides methods for creating a instance and initializing tables. -/// -public interface ITableServiceClientInitializer -{ - /// - /// Initialize table data store - /// - /// The instance to use for initialization. - /// The tableName set with fulltableNames . - /// A . - Task InitializeDataStoreAsync(TableServiceClient tableServiceClient, Dictionary tableList); -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Features/Storage/Models/Entities/HealthEntity.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Features/Storage/Models/Entities/HealthEntity.cs deleted file mode 100644 index 2410ec434e..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Features/Storage/Models/Entities/HealthEntity.cs +++ /dev/null @@ -1,32 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using Azure; -using Azure.Data.Tables; - -namespace Microsoft.Health.DicomCast.TableStorage.Features.Storage.Entities; - -/// -/// Entity used to check health of table storage -/// -public class HealthEntity : ITableEntity -{ - public HealthEntity() - { - } - - public HealthEntity(string partitionKey, string rowKey) - { - PartitionKey = partitionKey; - RowKey = rowKey; - } - - public string Data { get; set; } - public string PartitionKey { get; set; } - public string RowKey { get; set; } - public DateTimeOffset? Timestamp { get; set; } - public ETag ETag { get; set; } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Features/Storage/Models/Entities/IntransientEntity.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Features/Storage/Models/Entities/IntransientEntity.cs deleted file mode 100644 index 6fc2991006..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Features/Storage/Models/Entities/IntransientEntity.cs +++ /dev/null @@ -1,44 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using Azure; -using Azure.Data.Tables; -using EnsureThat; -using Microsoft.Health.DicomCast.Core.Features.ExceptionStorage; - -namespace Microsoft.Health.DicomCast.TableStorage.Features.Storage.Entities; - -/// -/// Entity used to represent a fhir intransient error -/// -public class IntransientEntity : IntransientError, ITableEntity -{ - public IntransientEntity() - { - } - - /// - /// Initializes a new instance of the class. - /// - /// StudyUid of the changefeed entry that failed - /// SeriesUid of the changefeed entry that failed - /// InstanceUid of the changefeed entry that failed - /// Changefeed sequence number that threw exception - /// The exception that was thrown - public IntransientEntity(string studyUid, string seriesUid, string instanceUid, long changeFeedSequence, Exception ex) - : base(studyUid, seriesUid, instanceUid, changeFeedSequence, ex) - { - EnsureArg.IsNotNull(ex, nameof(ex)); - - PartitionKey = ex.GetType().Name; - RowKey = Guid.NewGuid().ToString(); - } - - public string PartitionKey { get; set; } - public string RowKey { get; set; } - public DateTimeOffset? Timestamp { get; set; } - public ETag ETag { get; set; } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Features/Storage/Models/Entities/RetryableEntity.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Features/Storage/Models/Entities/RetryableEntity.cs deleted file mode 100644 index ec3dcba464..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Features/Storage/Models/Entities/RetryableEntity.cs +++ /dev/null @@ -1,42 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using Azure; -using Azure.Data.Tables; -using EnsureThat; -using Microsoft.Health.DicomCast.Core.Features.ExceptionStorage; - -namespace Microsoft.Health.DicomCast.TableStorage.Features.Storage.Models.Entities; - -public class RetryableEntity : RetryableError, ITableEntity -{ - public RetryableEntity() - { - } - - /// - /// Initializes a new instance of the class. - /// - /// StudyUid of the changefeed entry that failed - /// SeriesUid of the changefeed entry that failed - /// InstanceUid of the changefeed entry that failed - /// Changefeed sequence number that threw exception - /// Number of times changefeed entry has been retried - /// The exception that was thrown - public RetryableEntity(string studyUid, string seriesUid, string instanceUid, long changeFeedSequence, int retryNum, Exception ex) - : base(studyUid, seriesUid, instanceUid, changeFeedSequence, retryNum, ex) - { - EnsureArg.IsNotNull(ex, nameof(ex)); - - PartitionKey = ex.GetType().Name; - RowKey = Guid.NewGuid().ToString(); - } - - public string PartitionKey { get; set; } - public string RowKey { get; set; } - public DateTimeOffset? Timestamp { get; set; } - public ETag ETag { get; set; } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Features/Storage/Models/Entities/SyncStateEntity.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Features/Storage/Models/Entities/SyncStateEntity.cs deleted file mode 100644 index d624717ca7..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Features/Storage/Models/Entities/SyncStateEntity.cs +++ /dev/null @@ -1,39 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using Azure; -using Azure.Data.Tables; -using EnsureThat; -using Microsoft.Health.DicomCast.Core.Features.State; - -namespace Microsoft.Health.DicomCast.TableStorage.Features.Storage.Models.Entities; - -public class SyncStateEntity : ITableEntity -{ - public SyncStateEntity() - { - } - - /// - /// Initializes a new instance of the class. - /// - /// They SyncState - public SyncStateEntity(SyncState syncState) - { - EnsureArg.IsNotNull(syncState, nameof(syncState)); - - PartitionKey = Constants.SyncStatePartitionKey; - RowKey = Constants.SyncStateRowKey; - - SyncedSequence = syncState.SyncedSequence; - } - - public long SyncedSequence { get; set; } - public string PartitionKey { get; set; } - public string RowKey { get; set; } - public DateTimeOffset? Timestamp { get; set; } - public ETag ETag { get; set; } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Features/Storage/TableClientReadWriteTestProvider.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Features/Storage/TableClientReadWriteTestProvider.cs deleted file mode 100644 index 2ab5a24b19..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Features/Storage/TableClientReadWriteTestProvider.cs +++ /dev/null @@ -1,40 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using Azure.Data.Tables; -using EnsureThat; -using Microsoft.Health.DicomCast.TableStorage.Features.Storage.Entities; - -namespace Microsoft.Health.DicomCast.TableStorage.Features.Storage; - -public class TableClientReadWriteTestProvider : ITableClientTestProvider -{ - private const string TestPartitionKey = "testpartition"; - private const string TestRowKey = "testrow"; - private const string TestData = "testdata"; - private const string TestTable = "testTable"; - - private readonly TableServiceClient _testServiceClient; - - public TableClientReadWriteTestProvider(TableServiceClient testServiceClient) - { - _testServiceClient = EnsureArg.IsNotNull(testServiceClient, nameof(testServiceClient)); - } - - /// - public async Task PerformTestAsync(CancellationToken cancellationToken = default) - { - await _testServiceClient.CreateTableIfNotExistsAsync(TestTable, cancellationToken: cancellationToken); - - var tableClient = _testServiceClient.GetTableClient(TestTable); - var entity = new HealthEntity(TestPartitionKey, TestRowKey) { Data = TestData }; - - await tableClient.UpsertEntityAsync(entity, cancellationToken: cancellationToken); - - await tableClient.GetEntityAsync(TestPartitionKey, TestRowKey, cancellationToken: cancellationToken); - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Features/Storage/TableExceptionStore.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Features/Storage/TableExceptionStore.cs deleted file mode 100644 index 015f4b2a67..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Features/Storage/TableExceptionStore.cs +++ /dev/null @@ -1,137 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Azure.Data.Tables; -using EnsureThat; -using Microsoft.Extensions.Logging; -using Microsoft.Health.Dicom.Client.Models; -using Microsoft.Health.DicomCast.Core.Features.ExceptionStorage; -using Microsoft.Health.DicomCast.TableStorage.Features.Storage.Entities; -using Microsoft.Health.DicomCast.TableStorage.Features.Storage.Models.Entities; - -namespace Microsoft.Health.DicomCast.TableStorage.Features.Storage; - -/// -public class TableExceptionStore : IExceptionStore -{ - private readonly TableServiceClient _tableServiceClient; - private readonly ILogger _logger; - private readonly Dictionary _tableList; - - public TableExceptionStore( - TableServiceClientProvider tableServiceClientProvider, - ILogger logger) - { - EnsureArg.IsNotNull(tableServiceClientProvider, nameof(tableServiceClientProvider)); - EnsureArg.IsNotNull(logger, nameof(logger)); - - _tableServiceClient = tableServiceClientProvider.GetTableServiceClient(); - _tableList = tableServiceClientProvider.TableList; - _logger = logger; - } - - /// - public async Task WriteExceptionAsync(ChangeFeedEntry changeFeedEntry, Exception exceptionToStore, ErrorType errorType, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(changeFeedEntry, nameof(changeFeedEntry)); - - string tableName = errorType switch - { - ErrorType.FhirError => _tableList[Constants.FhirExceptionTableName], - ErrorType.DicomError => _tableList[Constants.DicomExceptionTableName], - ErrorType.DicomValidationError => _tableList[Constants.DicomValidationTableName], - ErrorType.TransientFailure => _tableList[Constants.TransientFailureTableName], - _ => null, - }; - - if (tableName == null) - { - Debug.Fail($"Error type of '{errorType}' is not supported."); - _logger.LogWarning("The error type '{ErrorType}' was not found so the exception wasn't recorded.", errorType); - return; - } - - string studyInstanceUid = changeFeedEntry.StudyInstanceUid; - string seriesInstanceUid = changeFeedEntry.SeriesInstanceUid; - string sopInstanceUid = changeFeedEntry.SopInstanceUid; - long changeFeedSequence = changeFeedEntry.Sequence; - - var tableClient = _tableServiceClient.GetTableClient(tableName); - var entity = new IntransientEntity(studyInstanceUid, seriesInstanceUid, sopInstanceUid, changeFeedSequence, exceptionToStore); - - try - { - await tableClient.UpsertEntityAsync(entity, cancellationToken: cancellationToken); - _logger.LogInformation("Error when processing changefeed entry: {ChangeFeedSequence} for DICOM instance with StudyUID: {StudyInstanceUid}, SeriesUID: {SeriesInstanceUid}, InstanceUID: {SopInstanceUid}. Stored into table: {Table} in table storage.", changeFeedSequence, studyInstanceUid, seriesInstanceUid, sopInstanceUid, tableName); - } - catch - { - _logger.LogInformation("Error when processing changefeed entry: {ChangeFeedSequence} for DICOM instance with StudyUID: {StudyInstanceUid}, SeriesUID: {SeriesInstanceUid}, InstanceUID: {SopInstanceUid}. Failed to store to table storage.", changeFeedSequence, studyInstanceUid, seriesInstanceUid, sopInstanceUid); - throw; - } - } - - /// - public async Task WriteRetryableExceptionAsync(ChangeFeedEntry changeFeedEntry, int retryNum, TimeSpan nextDelayTimeSpan, Exception exceptionToStore, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(changeFeedEntry, nameof(changeFeedEntry)); - - string studyInstanceUid = changeFeedEntry.StudyInstanceUid; - string seriesInstanceUid = changeFeedEntry.SeriesInstanceUid; - string sopInstanceUid = changeFeedEntry.SopInstanceUid; - long changeFeedSequence = changeFeedEntry.Sequence; - - var tableClient = _tableServiceClient.GetTableClient(_tableList[Constants.TransientRetryTableName]); - var entity = new RetryableEntity(studyInstanceUid, seriesInstanceUid, sopInstanceUid, changeFeedSequence, retryNum, exceptionToStore); - - try - { - await tableClient.UpsertEntityAsync(entity, cancellationToken: cancellationToken); - _logger.LogInformation("Retryable error when processing changefeed entry: {ChangeFeedSequence} for DICOM instance with StudyUID: {StudyInstanceUid}, SeriesUID: {SeriesInstanceUid}, InstanceUID: {SopInstanceUid}. Tried {RetryNum} time(s). Waiting {Milliseconds} milliseconds . Stored into table: {Table} in table storage.", changeFeedSequence, studyInstanceUid, seriesInstanceUid, sopInstanceUid, retryNum, nextDelayTimeSpan.TotalMilliseconds, _tableList[Constants.TransientRetryTableName]); - } - catch - { - _logger.LogInformation("Retryable error when processing changefeed entry: {ChangeFeedSequence} for DICOM instance with StudyUID: {StudyInstanceUid}, SeriesUID: {SeriesInstanceUid}, InstanceUID: {SopInstanceUid}. Tried {RetryNum} time(s). Failed to store to table storage.", changeFeedSequence, studyInstanceUid, seriesInstanceUid, sopInstanceUid, retryNum); - throw; - } - } - - public async Task<(IEnumerable, string)> ReadIntransientErrors(ErrorType errorType, string continuationToken = null, CancellationToken cancellationToken = default) - { - string tableName = errorType switch - { - ErrorType.FhirError => _tableList[Constants.FhirExceptionTableName], - ErrorType.DicomError => _tableList[Constants.DicomExceptionTableName], - ErrorType.DicomValidationError => _tableList[Constants.DicomValidationTableName], - ErrorType.TransientFailure => _tableList[Constants.TransientFailureTableName], - _ => throw new ArgumentOutOfRangeException(nameof(errorType)), - }; - - var tableClient = _tableServiceClient.GetTableClient(tableName); - - var result = tableClient.QueryAsync(cancellationToken: cancellationToken); - - var results = await result.AsPages(continuationToken).FirstOrDefaultAsync(cancellationToken); - - return (results?.Values ?? Enumerable.Empty(), results?.ContinuationToken); - } - - public async Task<(IEnumerable, string)> ReadRetryableErrors(ErrorType errorType, string continuationToken, CancellationToken cancellationToken = default) - { - var tableClient = _tableServiceClient.GetTableClient(_tableList[Constants.TransientRetryTableName]); - - var result = tableClient.QueryAsync(cancellationToken: cancellationToken); - - var results = await result.AsPages(continuationToken).FirstOrDefaultAsync(cancellationToken); - - return (results?.Values ?? Enumerable.Empty(), results?.ContinuationToken); - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Features/Storage/TableServiceClientInitializer.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Features/Storage/TableServiceClientInitializer.cs deleted file mode 100644 index 010497b69f..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Features/Storage/TableServiceClientInitializer.cs +++ /dev/null @@ -1,55 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Azure.Data.Tables; -using EnsureThat; -using Microsoft.Extensions.Logging; - -namespace Microsoft.Health.DicomCast.TableStorage.Features.Storage; - -public class TableServiceClientInitializer : ITableServiceClientInitializer -{ - private readonly ILogger _logger; - - public TableServiceClientInitializer( - ILogger logger) - { - _logger = EnsureArg.IsNotNull(logger, nameof(logger)); - } - - /// - public async Task InitializeDataStoreAsync(TableServiceClient tableServiceClient, Dictionary tableList) - { - EnsureArg.IsNotNull(tableServiceClient, nameof(tableServiceClient)); - EnsureArg.IsNotNull(tableList, nameof(tableList)); - - try - { - _logger.LogInformation("Initializing Table Storage and tables"); - - foreach (string tableName in tableList.Values) - { - if (await tableServiceClient.CreateTableIfNotExistsAsync(tableName) != null) - { - _logger.LogInformation("Created Table named '{TableName}'", tableName); - } - else - { - _logger.LogInformation("Table '{TableName}' already exists", tableName); - } - } - - _logger.LogInformation("Table Storage and tables successfully initialized"); - } - catch (Exception ex) - { - _logger.LogCritical(ex, "Table Storage and table initialization failed"); - throw; - } - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Features/Storage/TableServiceClientProvider.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Features/Storage/TableServiceClientProvider.cs deleted file mode 100644 index 132f11da7a..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Features/Storage/TableServiceClientProvider.cs +++ /dev/null @@ -1,103 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Azure.Data.Tables; -using EnsureThat; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Health.Core; -using Microsoft.Health.DicomCast.TableStorage.Configs; -using Microsoft.Health.Extensions.DependencyInjection; - -namespace Microsoft.Health.DicomCast.TableStorage.Features.Storage; - -public class TableServiceClientProvider : IHostedService, IRequireInitializationOnFirstRequest, IDisposable -{ - private readonly TableServiceClient _tableServiceClient; - private readonly RetryableInitializationOperation _initializationOperation; - private readonly TableDataStoreConfiguration _tableDataStoreConfiguration; - - // This table holds the full table names. - // Key contains list of Constants.alltables - // Values contains list of fulltable names computed as "tablenameprefix+key". TableNameprefix is dicomcastname provisioned. Its defualt value is empty. - public Dictionary TableList { get; } - - public TableServiceClientProvider( - TableServiceClient tableServiceClient, - ITableServiceClientInitializer tableServiceClientInitializer, - IOptions tableDataStoreConfiguration, - ILogger logger) - { - EnsureArg.IsNotNull(tableServiceClient, nameof(tableServiceClient)); - EnsureArg.IsNotNull(tableServiceClientInitializer, nameof(tableServiceClientInitializer)); - EnsureArg.IsNotNull(tableDataStoreConfiguration?.Value, nameof(tableDataStoreConfiguration)); - EnsureArg.IsNotNull(logger, nameof(logger)); - - _tableDataStoreConfiguration = tableDataStoreConfiguration?.Value; - - TableList = new Dictionary(); - InitializeTableNames(); - - _tableServiceClient = tableServiceClient; - _initializationOperation = new RetryableInitializationOperation( - () => tableServiceClientInitializer.InitializeDataStoreAsync(_tableServiceClient, TableList)); - } - - public Task StartAsync(CancellationToken cancellationToken) - { - // The result is ignored and will be awaited in EnsureInitialized(). Exceptions are logged within DocumentClientInitializer. - _ = _initializationOperation.EnsureInitialized().AsTask(); - - return Task.CompletedTask; - } - - public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; - - /// - /// Returns a task representing the initialization operation. Once completed, - /// this method will always return a completed task. If the task fails, the method - /// can be called again to retry the operation. - /// - /// A task representing the initialization operation. - public async Task EnsureInitialized() => await _initializationOperation.EnsureInitialized(); - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - _initializationOperation.Dispose(); - } - } - - public TableServiceClient GetTableServiceClient() - { - if (!_initializationOperation.IsInitialized) - { - _initializationOperation.EnsureInitialized().AsTask().GetAwaiter().GetResult(); - } - - return _tableServiceClient; - } - - private void InitializeTableNames() - { - foreach (var table in Constants.AllTables) - { - TableList.Add(table, $"{_tableDataStoreConfiguration.TableNamePrefix}{table}"); - } - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Features/Storage/TableStorageLocalEmulator.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Features/Storage/TableStorageLocalEmulator.cs deleted file mode 100644 index 84764b2f34..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Features/Storage/TableStorageLocalEmulator.cs +++ /dev/null @@ -1,11 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.DicomCast.TableStorage; - -internal static class TableStorageLocalEmulator -{ - public const string ConnectionString = "UseDevelopmentStorage=true"; -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Features/Storage/TableSyncStateStore.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Features/Storage/TableSyncStateStore.cs deleted file mode 100644 index bba25b5942..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Features/Storage/TableSyncStateStore.cs +++ /dev/null @@ -1,52 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Azure; -using Azure.Data.Tables; -using EnsureThat; -using Microsoft.Health.DicomCast.Core.Features.State; -using Microsoft.Health.DicomCast.TableStorage.Features.Storage.Models.Entities; - -namespace Microsoft.Health.DicomCast.TableStorage.Features.Storage.Models; - -public class TableSyncStateStore : ISyncStateStore -{ - private readonly TableServiceClient _tableServiceClient; - private readonly Dictionary _tableList; - - public TableSyncStateStore(TableServiceClientProvider tableServiceClientProvider) - { - EnsureArg.IsNotNull(tableServiceClientProvider, nameof(tableServiceClientProvider)); - - _tableServiceClient = tableServiceClientProvider.GetTableServiceClient(); - _tableList = tableServiceClientProvider.TableList; - } - - public async Task ReadAsync(CancellationToken cancellationToken = default) - { - TableClient tableClient = _tableServiceClient.GetTableClient(_tableList[Constants.SyncStateTableName]); - - try - { - var entity = await tableClient.GetEntityAsync(Constants.SyncStatePartitionKey, Constants.SyncStateRowKey, cancellationToken: cancellationToken); - return new SyncState(entity.Value.SyncedSequence, entity.Value.Timestamp.Value); - } - catch (RequestFailedException) - { - return SyncState.CreateInitialSyncState(); - } - } - - public async Task UpdateAsync(SyncState state, CancellationToken cancellationToken = default) - { - TableClient tableClient = _tableServiceClient.GetTableClient(_tableList[Constants.SyncStateTableName]); - var entity = new SyncStateEntity(state); - - await tableClient.UpsertEntityAsync(entity, cancellationToken: cancellationToken); - } -} diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Microsoft.Health.DicomCast.TableStorage.csproj b/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Microsoft.Health.DicomCast.TableStorage.csproj deleted file mode 100644 index c100690f34..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Microsoft.Health.DicomCast.TableStorage.csproj +++ /dev/null @@ -1,30 +0,0 @@ - - - - Azure Cosmos DB table storage utilities for Microsoft's DICOM Cast APIs. - $(LatestVersion) - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Properties/AssemblyInfo.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Properties/AssemblyInfo.cs deleted file mode 100644 index 299f677eb6..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,8 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; - -[assembly: CLSCompliant(false)] diff --git a/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Registration/DicomCastTableRegistrationExtension.cs b/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Registration/DicomCastTableRegistrationExtension.cs deleted file mode 100644 index 424f6e6b59..0000000000 --- a/converter/dicom-cast/src/Microsoft.Health.DicomCast.TableStorage/Registration/DicomCastTableRegistrationExtension.cs +++ /dev/null @@ -1,107 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using Azure.Identity; -using EnsureThat; -using Microsoft.Extensions.Azure; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Options; -using Microsoft.Health.DicomCast.Core.Features.ExceptionStorage; -using Microsoft.Health.DicomCast.TableStorage.Configs; -using Microsoft.Health.DicomCast.TableStorage.Features.Health; -using Microsoft.Health.DicomCast.TableStorage.Features.Storage; -using Microsoft.Health.DicomCast.TableStorage.Features.Storage.Models; -using Microsoft.Health.Extensions.DependencyInjection; - -namespace Microsoft.Health.DicomCast.TableStorage; - -public static class DicomCastTableRegistrationExtension -{ - public const string TableStoreConfigurationSectionName = "TableStore"; - - /// - /// Adds the table data store for dicom cast. - /// - /// Service collection - /// The configuration for the server. - /// An optional delegate to set properties after values have been loaded from configuration. - /// IServiceCollection - public static IServiceCollection AddTableStorageDataStore(this IServiceCollection serviceCollection, IConfiguration configuration, Action configureAction = null) - { - EnsureArg.IsNotNull(serviceCollection, nameof(serviceCollection)); - EnsureArg.IsNotNull(configuration, nameof(configuration)); - - serviceCollection - .AddTableDataStore(configuration, configureAction) - .AddHealthChecks().AddCheck(name: nameof(TableHealthCheck)); - - serviceCollection.Replace(new ServiceDescriptor(typeof(IExceptionStore), typeof(TableExceptionStore), ServiceLifetime.Singleton)); - - return serviceCollection; - } - - private static IServiceCollection AddTableDataStore(this IServiceCollection serviceCollection, IConfiguration configuration, Action configureAction = null) - { - EnsureArg.IsNotNull(serviceCollection, nameof(serviceCollection)); - EnsureArg.IsNotNull(configuration, nameof(configuration)); - - TableDataStoreConfiguration tableDataStoreConfiguration = RegisterTableDataStoreConfiguration(serviceCollection, configuration, configureAction); - - serviceCollection.AddAzureClients(builder => - { - if (string.IsNullOrWhiteSpace(tableDataStoreConfiguration.ConnectionString)) - { - builder.AddTableServiceClient(tableDataStoreConfiguration.EndpointUri) - .WithCredential(new DefaultAzureCredential(new DefaultAzureCredentialOptions { ManagedIdentityClientId = tableDataStoreConfiguration.ClientId })); - } - else - { - builder.AddTableServiceClient(tableDataStoreConfiguration.ConnectionString); - } - }); - - serviceCollection.Add() - .Singleton() - .AsSelf() - .AsService() - .AsService(); - - serviceCollection.Add() - .Singleton() - .AsService(); - - serviceCollection.Add() - .Singleton() - .AsSelf() - .AsImplementedInterfaces(); - - serviceCollection.Add() - .Singleton() - .AsSelf() - .AsImplementedInterfaces(); - - return serviceCollection; - } - - private static TableDataStoreConfiguration RegisterTableDataStoreConfiguration(IServiceCollection serviceCollection, IConfiguration configuration, Action configureAction = null) - { - var tableDataStoreConfiguration = new TableDataStoreConfiguration(); - configuration.GetSection(TableStoreConfigurationSectionName).Bind(tableDataStoreConfiguration); - - configureAction?.Invoke(tableDataStoreConfiguration); - - if (string.IsNullOrEmpty(tableDataStoreConfiguration.ConnectionString) && tableDataStoreConfiguration.EndpointUri == null) - { - tableDataStoreConfiguration.ConnectionString = TableStorageLocalEmulator.ConnectionString; - } - - serviceCollection.AddSingleton(Options.Create(tableDataStoreConfiguration)); - return tableDataStoreConfiguration; - } -} diff --git a/docker/Directory.Build.props b/docker/Directory.Build.props deleted file mode 100644 index 68664394e2..0000000000 --- a/docker/Directory.Build.props +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/docker/docker-compose.cast.yml b/docker/docker-compose.cast.yml deleted file mode 100644 index accbfeeb07..0000000000 --- a/docker/docker-compose.cast.yml +++ /dev/null @@ -1,35 +0,0 @@ -version: "3.8" - -services: - dicomcast: - build: - context: ./.. - dockerfile: converter/dicom-cast/src/Microsoft.Health.DicomCast.Hosting/Dockerfile - environment: - TableStore__ConnectionString: "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;TableEndpoint=http://azurite:10002/devstoreaccount1;" - Fhir__Endpoint: "http://fhir:8080" - DicomWeb__endpoint: "http://dicomserver:8080" - DicomCastWorker__PollInterval: "00:00:05" - Logging__Console__IncludeScopes: "true" - Patient__PatientSystemId: "patientSystemId" - Patient__IsIssuerIdUsed: false - restart: always - depends_on: - - azurite - - fhir - - dicomserver - - fhir: - image: healthplatformregistry.azurecr.io/r4_fhir-server:release - environment: - FHIRServer__Security__Enabled: "false" - SqlServer__ConnectionString: "Server=tcp:sql,1433;Initial Catalog=FHIR;Persist Security Info=False;User ID=sa;Password=${SAPASSWORD:-L0ca1P@ssw0rd};MultipleActiveResultSets=False;Connection Timeout=30;TrustServerCertificate=true" - SqlServer__AllowDatabaseCreation: "true" - SqlServer__Initialize: "true" - SqlServer__SchemaOptions__AutomaticUpdatesEnabled: "true" - DataStore: "SqlServer" - ports: - - "8081:8080" - restart: always - depends_on: - - sql diff --git a/docker/docker-compose.dcproj b/docker/docker-compose.dcproj deleted file mode 100644 index dcd53cef08..0000000000 --- a/docker/docker-compose.dcproj +++ /dev/null @@ -1,43 +0,0 @@ - - - - 2.1 - Linux - 336b1fb4-eef8-4e11-bdd5-818983d4e1cd - healthcare - LaunchBrowser - {Scheme}://localhost:{ServicePort} - dicomserver - docker-compose.https.yml;docker-compose.vs.yml - x64 - - - - - - - $(AdditionalComposeFilePaths);docker-compose.https.windows.yml - - - - - $(AdditionalComposeFilePaths);docker-compose.https.linux.yml - - - - - - - - - docker-compose.yml - - - - - - diff --git a/docker/docker-compose.features.yml b/docker/docker-compose.features.yml deleted file mode 100644 index 6c4a40aaa7..0000000000 --- a/docker/docker-compose.features.yml +++ /dev/null @@ -1,7 +0,0 @@ -version: "3.8" - -services: - dicomserver: - environment: - DicomServer__Features__EnableDataPartitions: "true" - DicomServer__Features__EnableLatestApiVersion: "true" diff --git a/docker/docker-compose.https.linux.yml b/docker/docker-compose.https.linux.yml deleted file mode 100644 index cd22697fd3..0000000000 --- a/docker/docker-compose.https.linux.yml +++ /dev/null @@ -1,6 +0,0 @@ -version: "3.8" - -services: - dicomserver: - volumes: - - ~/.aspnet/https:/root/.aspnet/https:r diff --git a/docker/docker-compose.https.windows.yml b/docker/docker-compose.https.windows.yml deleted file mode 100644 index 8cc49ed3c6..0000000000 --- a/docker/docker-compose.https.windows.yml +++ /dev/null @@ -1,6 +0,0 @@ -version: "3.8" - -services: - dicomserver: - volumes: - - ${APPDATA}/ASP.NET/Https:/root/.aspnet/https:r diff --git a/docker/docker-compose.https.yml b/docker/docker-compose.https.yml deleted file mode 100644 index e1cc5a2127..0000000000 --- a/docker/docker-compose.https.yml +++ /dev/null @@ -1,7 +0,0 @@ -version: "3.8" - -services: - dicomserver: - environment: - ASPNETCORE_URLS: "https://+:8080" - user: root diff --git a/docker/docker-compose.ports.azurite.yml b/docker/docker-compose.ports.azurite.yml deleted file mode 100644 index a090e673ce..0000000000 --- a/docker/docker-compose.ports.azurite.yml +++ /dev/null @@ -1,8 +0,0 @@ -version: "3.8" - -services: - azurite: - ports: - - "10001:10001" - - "10000:10000" - - "10002:10002" diff --git a/docker/docker-compose.vs.debug.yml b/docker/docker-compose.vs.debug.yml deleted file mode 100644 index b4f3a4293b..0000000000 --- a/docker/docker-compose.vs.debug.yml +++ /dev/null @@ -1,11 +0,0 @@ -version: "3.8" - -services: - dicomserver: - build: - args: - BUILD_CONFIGURATION: "Debug" - functions: - build: - args: - BUILD_CONFIGURATION: "Debug" diff --git a/docker/docker-compose.vs.release.yml b/docker/docker-compose.vs.release.yml deleted file mode 100644 index 98d71793c6..0000000000 --- a/docker/docker-compose.vs.release.yml +++ /dev/null @@ -1,11 +0,0 @@ -version: "3.8" - -services: - dicomserver: - build: - args: - BUILD_CONFIGURATION: "Release" - functions: - build: - args: - BUILD_CONFIGURATION: "Release" diff --git a/docker/docker-compose.vs.yml b/docker/docker-compose.vs.yml deleted file mode 100644 index 02df82254d..0000000000 --- a/docker/docker-compose.vs.yml +++ /dev/null @@ -1,16 +0,0 @@ -version: "3.8" - -services: - dicomserver: - image: ${DOCKER_REGISTRY-}microsofthealthdicomweb - functions: - image: ${DOCKER_REGISTRY-}microsofthealthdicomfunctions - azurite: - # Slightly different ports are used as to not collide with any currently running storage emulator in Visual Studio - ports: - - "10001:10011" - - "10000:10010" - - "10002:10012" - sql: - ports: - - "1433:1433" diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml deleted file mode 100644 index 8c47f8a648..0000000000 --- a/docker/docker-compose.yml +++ /dev/null @@ -1,76 +0,0 @@ -version: "3.8" - -services: - dicomserver: - build: - context: ./.. - dockerfile: src/Microsoft.Health.Dicom.Web/Dockerfile - args: - BUILD_CONFIGURATION: Release - CONTINUOUS_INTEGRATION_BUILD: ${ContinuousIntegrationBuild:-false} - platform: linux/amd64 - environment: - AzureWebJobsStorage: "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://azurite:10000/devstoreaccount1;QueueEndpoint=http://azurite:10001/devstoreaccount1;TableEndpoint=http://azurite:10002/devstoreaccount1;" - ASPNETCORE_ENVIRONMENT: "Development" - ASPNETCORE_URLS: "http://+:8080" - BlobStore__ConnectionString: "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://azurite:10000/devstoreaccount1;" - DicomFunctions__DurableTask__ConnectionName: "AzureWebJobsStorage" - DicomFunctions__Indexing__Batching__MaxParallelCount: "1" - SqlServer__AllowDatabaseCreation: "true" - SqlServer__ConnectionString: "Server=tcp:sql,1433;Initial Catalog=Dicom;Persist Security Info=False;User ID=sa;Password=${SAPASSWORD:-L0ca1P@ssw0rd};MultipleActiveResultSets=False;Connection Timeout=30;TrustServerCertificate=true" - SqlServer__Initialize: "true" - ports: - - "8080:8080" - restart: on-failure - depends_on: - - functions - - azurite - - sql - functions: - build: - # While Container Tools are said to support Azure Functions, it does not appear that - # Docker Compose projects support them. So for now the Dockerfile is kept in a folder separate from the project file - context: ./.. - dockerfile: src/Microsoft.Health.Dicom.Functions.App/Docker/Dockerfile - args: - BUILD_CONFIGURATION: Release - CONTINUOUS_INTEGRATION_BUILD: ${ContinuousIntegrationBuild:-false} - platform: linux/amd64 - environment: - AzureFunctionsJobHost__BlobStore__ConnectionString: "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://azurite:10000/devstoreaccount1;" - AzureFunctionsJobHost__Logging__Console__IsEnabled: "true" - AzureFunctionsJobHost__SqlServer__ConnectionString: "Server=tcp:sql,1433;Initial Catalog=Dicom;Persist Security Info=False;User ID=sa;Password=${SAPASSWORD:-L0ca1P@ssw0rd};MultipleActiveResultSets=False;Connection Timeout=30;TrustServerCertificate=true" - AzureWebJobsStorage: "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://azurite:10000/devstoreaccount1;QueueEndpoint=http://azurite:10001/devstoreaccount1;TableEndpoint=http://azurite:10002/devstoreaccount1;" - AZURE_FUNCTIONS_ENVIRONMENT: "Development" - WEBSITE_HOSTNAME: "localhost:8080" - APPINSIGHTS_INSTRUMENTATIONKEY: "00000000-0000-0000-0000-000000000000" # required to configure telemetry client even when running locally - ports: - - "7072:8080" - restart: on-failure - depends_on: - - azurite - azurite: - # See here for tags: https://mcr.microsoft.com/en-us/product/azure-storage/azurite/tags - image: mcr.microsoft.com/azure-storage/azurite:latest - # # These port bindings [source]:[dest] can be uncommented to connect to the storage emulator via Microsoft Azure Storage Explorer - # # Note that the source ports may need to change if a storage emulator is already running on localhost -# ports: -# - "10001:10001" -# - "10000:10000" -# - "10002:10002" - sql: - build: - context: ./.. - dockerfile: docker/sql/Dockerfile - environment: - SA_PASSWORD: ${SAPASSWORD:-L0ca1P@ssw0rd} - ACCEPT_EULA: "Y" - healthcheck: - test: ["CMD", "/opt/mssql-tools/bin/sqlcmd", "-U", "sa", "-P", "${SAPASSWORD:-L0ca1P@ssw0rd}", "-Q", "SELECT * FROM INFORMATION_SCHEMA.TABLES"] - interval: 10s - timeout: 10s - retries: 6 - start_period: 15s - # # These port bindings [source]:[dest] can be uncommented to connect to SQL Server via Microsoft SQL Management Studio - # ports: - # - "1433:1433" diff --git a/docker/sql/Dockerfile b/docker/sql/Dockerfile deleted file mode 100644 index cf59297ce0..0000000000 --- a/docker/sql/Dockerfile +++ /dev/null @@ -1,33 +0,0 @@ -# Dockerfile based on twright-msft's Dockerfile here: -# https://github.com/Microsoft/mssql-docker/blob/master/linux/preview/examples/mssql-agent-fts-ha-tools/Dockerfile - -# Instructions for installation based on Microsoft's documentation here: -# https://docs.microsoft.com/en-us/sql/linux/quickstart-install-connect-ubuntu?view=sql-server-ver15 - -# Ubuntu 20.04 LTS ("Focal Fossa") -# MS SQL Sever does not yet support later versions -FROM ubuntu:focal@sha256:0b897358ff6624825fb50d20ffb605ab0eaea77ced0adb8c6a4b756513dec6fc - -# Install SQL Server 2019 and after its prerequisites -RUN export DEBIAN_FRONTEND=noninteractive && \ - apt-get update && \ - apt-get install -yq curl apt-transport-https gnupg && \ - # Get official Microsoft repository configuration - curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add - && \ - curl https://packages.microsoft.com/config/ubuntu/20.04/mssql-server-2019.list | tee /etc/apt/sources.list.d/mssql-server.list && \ - curl https://packages.microsoft.com/config/ubuntu/20.04/prod.list | tee /etc/apt/sources.list.d/msprod.list && \ - # Install SQL Server - apt-get update && \ - apt-get install -y mssql-server && \ - # Install Full Text Search (FTS) - apt-get install -y mssql-server-fts && \ - # Install SQL Tools - ACCEPT_EULA=Y apt-get install -y mssql-tools unixodbc-dev && \ - # Clean up the Dockerfile - apt-get clean && \ - rm -rf /var/lib/apt/lists - -EXPOSE 1433 - -# Run SQL Server process -CMD /opt/mssql/bin/sqlservr diff --git a/docs/Versioning.md b/docs/Versioning.md deleted file mode 100644 index fe8842cdda..0000000000 --- a/docs/Versioning.md +++ /dev/null @@ -1,50 +0,0 @@ -# Semantic Versioning for Dicom Server - -This guide gives an overview of the Semantic versioning implementation in use with this project. -To achieve semantic versioning consistently and reliably the [GitVersion](https://github.com/GitTools/GitVersion) library is used. - -## Git Version - -### Overview -GitVersion is a software library and build task that uses Git history to calculate the version that should be used for the current build. The following sections explain how it is configured and the commands available to assist in versioning. - -### Setup -A [configuration](https://github.com/microsoft/dicom-server/blob/master/GitVersion.yml) file is included in the root directory that is used to setup the version strategy and specify how versioning should be calculated against the default and other branches. Currently, all commits to main will be treated as a release, all commits to other branches (including pull requests) will be treated as pre-release (e.g. `1.2.0-my-branch+1`). - -The configured GitVersion versioning strategy is [mainline development](https://gitversion.net/docs/reference/versioning-modes/mainline-development), which increments the patch version on every commit to the main branch. Our current development workflow assumes that the main branch will stage a release on every commit, some releases however will be not be approved. - -When a release is approved this should result in the assets being published to the nuget feed and a tag being created against the code to mark the release. - -### Commands -Several commands are available during the squash-merge to allow incrementing the major/minor release numbers. - -For a major feature or major breaking changes, the following commands can be added to the commit message: -``` -+semver: breaking -or -+semver: major -``` - -Smaller changes can choose to increment the minor version: -``` -+semver: feature -or -+semver: minor -``` - -For bug fixes or other incremental changes, nothing needs to be added, this will happen automatically. - -## Examples of when to increment versions - -| Action | Command | -|---|---| -| Updated a minor nuget package version | None :beach_umbrella: | -| Fixed a bug | None :beach_umbrella: | -| Small backwards-compatible change | None :beach_umbrella: :tropical_drink: | -| Updated documentation | None or `+semver: skip` | -| Adding a new feature/component/library | `+semver: feature` :bowtie: | -| Major product-level change | `+semver: major` | -| Incompatible binary change | `+semver: major` :boom: | -
- -:exclamation: Note: The Assembly version is using the Major version with static Minor and Patch versions (e.g. {major}.0.0), so using `+semver: major` will force downstream applications to be recompiled. Incrementing the Minor or Patch versions will keep resulting assemblies binary-compatible. \ No newline at end of file diff --git a/docs/api-versioning.md b/docs/api-versioning.md deleted file mode 100644 index 51cee3aab8..0000000000 --- a/docs/api-versioning.md +++ /dev/null @@ -1,65 +0,0 @@ -# API Versioning for DICOM Server - -This guide gives an overview of the API version policies for DICOM Server. - -All versions of the DICOM APIs will always conform to the DICOMweb™ Standard specifications, but versions may expose different APIs based on our [conformance statement](https://github.com/microsoft/dicom-server/blob/main/docs/resources/conformance-statement.md). - -## Specifying version of REST API in Requests - -The version of the REST API must be explicitly specified in the request URL as in the following example: - -`https:///v/studies` - -**Note:** Routes without a version are no longer supported. - - -### Supported Versions - -Currently the supported versions are: -- v1.0-prerelease -- v1 - -The OpenApi Doc for the supported versions can be found at the following url: `https:///{version}/api.yaml` - - -### Prerelease versions - -An API version with the label "prerelease" indicates that the version is not ready for production, and should only be used in testing environments. These endpoints may experience breaking changes without notice. - -### How Versions Are Incremented - -We currently only increment the major version whenever there is a breaking change which is considered to be not backwards compatible. - -Some Examples of a breaking change (Major version is incremented): -1. Renaming or removing endpoints -2. Removing parameters or adding mandatory parameters -3. Changing status code -4. Deleting property in response or altering response type at all (but okay to add properties to the response) -5. Changing the type of a property -6. Behavior of an API changes (changes in business logic, used to do foo, now does bar) - -Non-breaking changes (Version is not incremented): -1. Addition of properties that are nullable or have a default value -2. Addition of properties to a response model -3. Changing the order of properties - -### Headers in responses - -`ReportApiVersions` is turned on, which means we will return the headers `api-supported-versions` and `api-deprecated-versions` when appropriate. - -- `api-supported-versions` will list which versions are supported for the requested API. It is only returned when calling an endpoint annotated with `[ApiVersion("")]`. - -- `api-deprecated-versions` will list which versions have been deprecated for the requested API. It is only returned when calling an endpoint annotated with `[ApiVersion("", Deprecated = true)]`. - -Example: - -``` -[ApiVersion("1")] -[ApiVersion("1.0-prerelease", Deprecated = true)] -``` - -![Response headers](images/api-headers-example.PNG) - -### API documentation / Swagger Update -Be sure to make appropriate updates to swagger files and add new version checks where necessary. Information on where and -how to do this is [here](./resources/swagger.md). diff --git a/docs/concepts/bulk-update.md b/docs/concepts/bulk-update.md deleted file mode 100644 index 97e0a8cdfa..0000000000 --- a/docs/concepts/bulk-update.md +++ /dev/null @@ -1,235 +0,0 @@ -# Bulk update overview -Bulk update is a feature that enables updates of DICOM attributes/metadata without needing to delete and re-add. The currently supported attributes include those in the [Patient Identification Module](https://dicom.nema.org/dicom/2013/output/chtml/part03/sect_C.2.html#table_C.2-2), [Patient Demographic Module](https://dicom.nema.org/dicom/2013/output/chtml/part03/sect_C.2.html#table_C.2-3) and the [General Study Module](https://dicom.nema.org/medical/dicom/2020b/output/chtml/part03/sect_C.7.2.html#table_C.7-3) that are not sequences, also listed below. - -**Patient Identification Module** -| Attribute Name | Tag | Description | -| ---------------- | --------------| --------------------- | -| Patient's Name | (0010,0010) | Patient's full name | -| Patient ID | (0010,0020) | Primary hospital identification number or code for the patient. | -| Other Patient IDs| (0010,1000) | Other identification numbers or codes used to identify the patient. -| Type of Patient ID| (0010,0022) | The type of identifier in this item. Enumerated Values: TEXT RFID BARCODE Note The identifier is coded as a string regardless of the type, not as a binary value. -| Other Patient Names| (0010,1001) | Other names used to identify the patient. -| Patient's Birth Name| (0010,1005) | Patient's birth name. -| Patient's Mother's Birth Name| (0010,1060) | Birth name of patient's mother. -| Medical Record Locator | (0010,1090) | An identifier used to find the patient's existing medical record (e.g., film jacket). -| Issuer of Patient ID | (0010,0021) | Identifier of the Assigning Authority (system, organization, agency, or department) that issued the Patient ID. - -**Patient Demographic Module** -| Attribute Name | Tag | Description | -| ---------------- | --------------| --------------------- | -| Patient's Age | (0010,1010) | Age of the Patient. | -| Occupation | (0010,2180) | Occupation of the Patient. | -| Confidentiality Constraint on Patient Data Description | (0040,3001) | Special indication to the modality operator about confidentiality of patient information (e.g., that he should not use the patients name where other patients are present). | -| Patient's Birth Date | (0010,0030) | Date of birth of the named patient | -| Patient's Birth Time | (0010,0032) | Time of birth of the named patient | -| Patient's Sex | (0010,0040) | Sex of the named patient. | -| Quality Control Subject |(0010,0200) | Indicates whether or not the subject is a quality control phantom. | -| Patient's Size | (0010,1020) | Patient's height or length in meters | -| Patient's Weight | (0010,1030) | Weight of the patient in kilograms | -| Patient's Address | (0010,1040) | Legal address of the named patient | -| Military Rank | (0010,1080) | Military rank of patient | -| Branch of Service | (0010,1081) | Branch of the military. The country allegiance may also be included (e.g., U.S. Army). | -| Country of Residence | (0010,2150) | Country in which patient currently resides | -| Region of Residence | (0010,2152) | Region within patient's country of residence | -| Patient's Telephone Numbers | (0010,2154) | Telephone numbers at which the patient can be reached | -| Ethnic Group | (0010,2160) | Ethnic group or race of patient | -| Patient's Religious Preference | (0010,21F0) | The religious preference of the patient | -| Patient Comments | (0010,4000) | User-defined comments about the patient | -| Responsible Person | (0010,2297) | Name of person with medical decision making authority for the patient. | -| Responsible Person Role | (0010,2298) | Relationship of Responsible Person to the patient. | -| Responsible Organization | (0010,2299) | Name of organization with medical decision making authority for the patient. | -| Patient Species Description | (0010,2201) | The species of the patient. | -| Patient Breed Description | (0010,2292) | The breed of the patient.See Section C.7.1.1.1.1. | -| Breed Registration Number | (0010,2295) | Identification number of a veterinary patient within the registry. | - -**General study module** -| Attribute Name | Tag | Description | -| ---------------- | --------------| --------------------- | -| Referring Physician's Name | (0008,0090) | Name of the Patient's referring physician | -| Accession Number | (0008,0050) | A RIS generated number that identifies the order for the Study. | -| Study Description | (0008,1030) | Institution-generated description or classification of the Study (component) performed. - -After a study is updated, there are two versions of the instances that can be retrieved: the original, unmodified instances and the latest version with updated attributes. Intermediate versions are not persisted. - -## API Design -Following URIs assume an implicit DICOM service base URI. For example, the base URI of a DICOM server running locally would be `https://localhost:63838/`. -Example requests can be sent in the [Postman collection](../resources/Conformance-as-Postman.postman_collection.json). - -### Bulk update study -Bulk update endpoint starts a long running operation that updates all the instances in the study with the specified attributes. - -```http -POST ...v2/studies/$bulkUpdate -POST ...v2/partitions/{PartitionName}/studies/$bulkUpdate -``` - -#### Request Header - -| Name | Required | Type | Description | -| ------------ | --------- | ------ | ------------------------------- | -| Content-Type | False | string | `application/json` is supported | - -#### Request Body - -Below `UpdateSpecification` is passed as the request body. The `UpdateSpecification` needs both `studyInstanceUids` and `changeDataset` to be specified. - -```json -{ - "studyInstanceUids": ["1.113654.3.13.1026"], - "changeDataset": { - "00100010": { - "vr": "PN", - "Value": - [ - { - "Alphabetic": "New Patient Name 1" - } - ] - } - } -} -``` - -#### Responses -Upon successfully starting an bulk update operation, the bulk update API returns a `202` status code. The body of the response contains a reference to the operation. - -```http -HTTP/1.1 202 Accepted -Content-Type: application/json -{ - "id": "1323c079a1b64efcb8943ef7707b5438", - "href": "../v2/operations/1323c079a1b64efcb8943ef7707b5438" -} -``` - -| Name | Type | Description | -| ----------------- | ------------------------------------------- | ------------------------------------------------------------ | -| 202 (Accepted) | [Operation Reference](#operation-reference) | A long-running operation has been started to update DICOM attributes | -| 400 (Bad Request) | | Request body has invalid data | - -### Operation Status -The above `href` URL can be polled for the current status of the export operation until completion. A terminal state is signified by a `200` status instead of `202`. - -```http -GET .../operations/{operationId} -``` - -#### URI Parameters - -| Name | In | Required | Type | Description | -| ----------- | ---- | -------- | ------ | ---------------- | -| operationId | path | True | string | The operation id | - -#### Responses - -**Successful response** - -```json -{ - "operationId": "1323c079a1b64efcb8943ef7707b5438", - "type": "update", - "createdTime": "2023-05-08T05:01:30.1441374Z", - "lastUpdatedTime": "2023-05-08T05:01:42.9067335Z", - "status": "completed", - "percentComplete": 100, - "results": { - "studyUpdated": 1, - "instanceUpdated": 16 - } -} -``` - -**Failure respose** -``` -{ - "operationId": "1323c079a1b64efcb8943ef7707b5438", - "type": "update", - "createdTime": "2023-05-08T05:01:30.1441374Z", - "lastUpdatedTime": "2023-05-08T05:01:42.9067335Z", - "status": "failed", - "percentComplete": 100, - "results": { - "studyUpdated": 0, - "studyFailed": 1, - "instanceUpdated": 0, - "errors": [ - "Failed to update instances for study 1.113654.3.13.1026" - ] - } -} -``` - -If there are any instance specific exception, it will be added to the `errors` list. It will include all the UIDs of the instance like -`Instance UIDs - PartitionKey: 1, StudyInstanceUID: 1.113654.3.13.1026, SeriesInstanceUID: 1.113654.3.13.1035, SOPInstanceUID: 1.113654.3.13.1510` - -| Name | Type | Description | -| --------------- | ----------------------- | -------------------------------------------- | -| 200 (OK) | [Operation](#operation) | The operation with the specified ID has completed | -| 202 (Accepted) | [Operation](#operation) | The operation with the specified ID is running | -| 404 (Not Found) | | Operation not found | - -## Retrieve (WADO-RS) - -The Retrieve Transaction feature provides the ability to retrieve stored studies, series, and instances, including both the original and latest versions. - -> Note: The supported endpoints for retrieving instances and metadata are listed below. - -| Method | Path | Description | -| :----- | :---------------------------------------------------------------------- | :---------- | -| GET | ../studies/{study} | Retrieves all instances within a study. | -| GET | ../studies/{study}/metadata | Retrieves the metadata for all instances within a study. | -| GET | ../studies/{study}/series/{series} | Retrieves all instances within a series. | -| GET | ../studies/{study}/series/{series}/metadata | Retrieves the metadata for all instances within a series. | -| GET | ../studies/{study}/series/{series}/instances/{instance} | Retrieves a single instance. | -| GET | ../studies/{study}/series/{series}/instances/{instance}/metadata | Retrieves the metadata for a single instance. | - -In order to retrieve original version, `msdicom-request-original` header should be set to `true`. - -Example request to retrieve an orifginal version of an instance is shown below. - -```http -GET .../studies/{study}/series/{series}/instances/{instance} -Accept: multipart/related; type="application/dicom"; transfer-syntax=* -msdicom-request-original: true -Content-Type: application/dicom - ``` -## Delete - -This transaction will delete both original and latest version of instances. - -> Note: After a Delete transaction the both the deleted instances will not be recoverable. - -## Change Feed - -Along with other actions (Create and Delete), Updated change feed action is populated for every update operation. - -Examples of the request and response of change feed action can be found [here](./change-feed.md). - -### Other APIs - -There is no change in other APIs. All the other APIs supports only latest version of instances. - -### What changed in DICOM file - -As part of bulk update, only DICOM metadata is updated. The pixel data is not updated. Pixel data will be same as the original version. - -Other than updating the metadata, the file meta information of the DICOM file is updated with the below information. - -| Tag | Attribute name | Description | Value -| --------------| --------------------- | --------------------- | --------------| -| (0002,0012) | Implementation Class UID | Uniquely identifies the implementation that wrote this file and its content. | 1.3.6.1.4.1.311.129 | -| (0002,0013) | Implementation Version Name | Identifies a version for an Implementation Class UID (0002,0012) | Assembly version of the DICOM service (e.g. 0.1.4785) | - -Here, the UID `1.3.6.1.4.1.311.129` is a registered under [Microsoft OID arc](https://oidref.com/1.3.6.1.4.1.311) in IANA. - -#### Limitations - -> Only Patient identificaton and demographic attributes are supported for bulk update. - -> Maximum of 50 studies can be updated at once. - -> Only one update operation can be performed at a time. - -> There is no way to delete only the latest version or revert back to original version. - -> We do not support updating any field from non-null to a null value. diff --git a/docs/concepts/change-feed.md b/docs/concepts/change-feed.md deleted file mode 100644 index ac314ac6b0..0000000000 --- a/docs/concepts/change-feed.md +++ /dev/null @@ -1,220 +0,0 @@ -# Change Feed Overview - -The Change Feed provides logs of all the changes that occur in your Medical Imaging Server for DICOM. The Change Feed provides ordered, guaranteed, immutable, read-only log of these changes. The Change Feed offers the ability to go through the history of the Medical Imaging Server for DICOM and act upon the creates and deletes in the service. - -Client applications can read these logs at any time in batches of any size. The Change Feed enables you to build efficient and scalable solutions that process change events that occur in your Medical Imaging Server for DICOM. - -You can process these change events asynchronously, incrementally, or in-full. Any number of client applications can independently read the Change Feed, in parallel, and at their own pace. - -As of v2 of the API, the Change Feed can be queried for a particular time window. - -## API Design - -The API exposes two `GET` endpoints for interacting with the Change Feed. A typical flow for consuming the Change Feed is [provided below](#example-usage-flow). - -Verb | Route | Returns | Description -:--- | :----------------- | :---------- | :--- -GET | /changefeed | Json Array | [Read the Change Feed](#read-change-feed) -GET | /changefeed/latest | Json Object | [Read the latest entry in the Change Feed](#get-latest-change-feed-item) - -### Object model - -Field | Type | Description -:------------------ | :-------- | :--- -Sequence | long | The unique ID per change events -StudyInstanceUid | string | The study instance UID -SeriesInstanceUid | string | The series instance UID -SopInstanceUid | string | The sop instance UID -Action | string | The action that was performed - either `create` or `delete` -Timestamp | datetime | The date and time the action was performed in UTC -State | string | [The current state of the metadata](#states) -Metadata | object | Optionally, the current DICOM metadata if the instance exists - -#### States - -State | Description -:------- | :--- -current | This instance is the current version. -replaced | This instance has been replaced by a new version. -deleted | This instance has been deleted and is no longer available in the service. - -## Change Feed -The Change Feed resource is a collection of events that have occurred within the DICOM server. - -### Version 2 - -#### Request -```http -GET /changefeed?startTime={datetime}&endtime={datetime}&offset={int}&limit={int}&includemetadata={bool} HTTP/1.1 -Accept: application/json -Content-Type: application/json -``` - -#### Response -```json -[ - { - "Sequence": 1, - "StudyInstanceUid": "{uid}", - "SeriesInstanceUid": "{uid}", - "SopInstanceUid": "{uid}", - "Action": "create|delete", - "Timestamp": "2020-03-04T01:03:08.4834Z", - "State": "current|replaced|deleted", - "Metadata": { - // DICOM JSON - } - }, - { - "Sequence": 2, - "StudyInstanceUid": "{uid}", - "SeriesInstanceUid": "{uid}", - "SopInstanceUid": "{uid}", - "Action": "create|delete", - "Timestamp": "2020-03-05T07:13:16.4834Z", - "State": "current|replaced|deleted", - "Metadata": { - // DICOM JSON - } - }, - // ... -] -``` - -#### Parameters - -Name | Type | Description | Default | Min | Max | -:-------------- | :------- | :---------- | :------ | :-- | :-- | -offset | long | The number of events to skip from the beginning of the result set | `0` | `0` | | -limit | int | The maximum number of events to return | `100` | `1` | `200` | -startTime | DateTime | The inclusive start time for change events | `"0001-01-01T00:00:00Z"` | `"0001-01-01T00:00:00Z"` | `"9999-12-31T23:59:59.9999998Z"`| -endTime | DateTime | The exclusive end time for change events | `"9999-12-31T23:59:59.9999999Z"` | `"0001-01-01T00:00:00.0000001"` | `"9999-12-31T23:59:59.9999999Z"` | -includeMetadata | bool | Indicates whether or not to include the DICOM metadata | `true` | | | - -### Version 1 - -#### Request -```http -GET /changefeed?offset={int}&limit={int}&includemetadata={bool} HTTP/1.1 -Accept: application/json -Content-Type: application/json -``` - -#### Response -```json -[ - { - "Sequence": 1, - "StudyInstanceUid": "{uid}", - "SeriesInstanceUid": "{uid}", - "SopInstanceUid": "{uid}", - "Action": "create|delete|update", - "Timestamp": "2020-03-04T01:03:08.4834Z", - "State": "current|replaced|deleted", - "Metadata": { - // DICOM JSON - } - }, - { - "Sequence": 2, - "StudyInstanceUid": "{uid}", - "SeriesInstanceUid": "{uid}", - "SopInstanceUid": "{uid}", - "Action": "create|delete|update", - "Timestamp": "2020-03-05T07:13:16.4834Z", - "State": "current|replaced|deleted", - "Metadata": { - // DICOM JSON - } - }, - // ... -] -``` - -#### Parameters -Name | Type | Description | Default | Min | Max | -:-------------- | :------- | :---------- | :------ | :-- | :-- | -offset | long | The exclusive starting sequence number for events | `0` | `0` | | -limit | int | The maximum value of the sequence number relative to the offset. For example, if the offset is 10 and the limit is 5, then the maximum sequence number returned will be 15. | `10` | `1` | `100` | -includeMetadata | bool | Indicates whether or not to include the DICOM metadata | `true` | | | - -## Latest Change Feed -The latest Change Feed resource represents the latest event that has occurred within the DICOM Server. - -### Request -```http -GET /changefeed/latest?includemetadata={bool} HTTP/1.1 -Accept: application/json -Content-Type: application/json -``` - -### Response -```json -{ - "Sequence": 2, - "StudyInstanceUid": "{uid}", - "SeriesInstanceUid": "{uid}", - "SopInstanceUid": "{uid}", - "Action": "create|delete|update", - "Timestamp": "2020-03-05T07:13:16.4834Z", - "State": "current|replaced|deleted", - "Metadata": { - // DICOM JSON - } -} -``` - -### Parameters - -Name | Type | Description | Default | -:-------------- | :--- | :---------- | :------ | -includeMetadata | bool | Indicates whether or not to include the metadata | `true` | - -## Usage - -### DICOMcast - -[DICOMcast](/converter/dicom-cast) is a stateful processor that pulls DICOM changes from Change Feed, transforms and publishes them to a configured Azure API for FHIR service as an [`ImagingStudy` resource](https://www.hl7.org/fhir/imagingstudy.html). DICOM Cast can start processing the DICOM change events at any point and continue to pull and process new changes incrementally. - -### User Application - -Below is the flow for an example application that wants to do additional processing on the instances within the DICOM service. - -#### Version 2 - -1. An application regularly queries the Change Feed on some time interval - * For example, if querying every hour, a query for the Change Feed may look like `/changefeed?startTime=2023-05-10T16:00:00Z&endTime=2023-05-10T17:00:00Z` - * If starting from the beginning, the Change Feed query may omit the `startTime` to read all of the changes up to, but excluding, the `endTime` - * E.g. `/changefeed?endTime=2023-05-10T17:00:00Z` -2. Based on the `limit` (if provided), an application continues to query for additional pages of change events if the number of returned events is equal to the `limit` (or default) by updating the offset on each subsequent query - * For example, if the `limit` is `100`, and 100 events are returned, then the subsequent query would include `offset=100` to fetch the next "page" of results. The below queries demonstrate the pattern: - * `/changefeed?offset=0&limit=100&startTime=2023-05-10T16:00:00Z&endTime=2023-05-10T17:00:00Z` - * `/changefeed?offset=100&limit=100&startTime=2023-05-10T16:00:00Z&endTime=2023-05-10T17:00:00Z` - * `/changefeed?offset=200&limit=100&startTime=2023-05-10T16:00:00Z&endTime=2023-05-10T17:00:00Z` - * If fewer events than the `limit` are returned, then the application can assume that there are no more results within the time range - -#### Version 1 - -1. An application determines from which sequence number it wishes to start reading change events: - * To start from the first event, the application should use `offset=0` - * To start from the latest event, the application should specify the `offset` parameter with the value of `Sequence` from the latest change event using the `/changefeed/latest` resource -2. On some regular polling interval, the application performs the following actions: - * Fetches the latest sequence number from the `/changefeed/latest` endpoint - * Fetches the next set of changes for processing by querying the change feed with the current offset - * For example, if the application has currently processed up to sequence number 15 and it only wants to process at most 5 events at once, then it should use the URL `/changefeed?offset=15&limit=5` - * Processes any entries return by the `/changefeed` resource - * Updates its current sequence number to either: - 1. The maximum sequence number returned by the `/changefeed` resource - 2. The `offset` + `limit` if no change events were returned from the `/changefeed` resource, but the latest sequence number returned by `/changefeed/latest` is greater than the current sequence number used for `offset` - -### Other potential usage patterns - -Change Feed support is well-suited for scenarios that process data based on objects that have changed. For example, it can be used to: - -* Build connected application pipelines like ML that react to change events or schedule executions based on created or deleted instance. -* Extract business analytics insights and metrics, based on changes that occur to your objects. -* Poll the Change Feed to create an event source for push notifications. - -## Summary - -In this Concept, we reviewed the REST API design of Change Feed and potential usage scenarios. For a how-to guide on Change Feed, see [Pull changes from Change Feed](../how-to-guides/pull-changes-from-change-feed.md). diff --git a/docs/concepts/data-partitions.md b/docs/concepts/data-partitions.md deleted file mode 100644 index 0b7f695aee..0000000000 --- a/docs/concepts/data-partitions.md +++ /dev/null @@ -1,82 +0,0 @@ -# Overview of Data Partitions - -Data partitioning is an optional feature that can be enabled for a DICOM service. It implements a light-weight data partition scheme that enables customers to store multiple copies of the same image with the same identifying instance UIDs on a single DICOM service. - -While UIDs **should** be [unique across all contexts](http://dicom.nema.org/dicom/2013/output/chtml/part05/chapter_9.html), it's common practice for DICOM files to be written to portable storage media by a healthcare provider and given to a patient, who then gives the files to another healthcare provider, who then transfers the files into a new DICOM storage system. Thus, multiple copies of one DICOM file commonly exist in isolated DICOM systems. As DICOM functionality moves to the cloud, unifying previously disconnected systems, data partitioning can provide an on-ramp for your existing data stores and workflows. - -## Feature Enablement -The data partitions feature can be enabled by setting the configuration key `DicomServer:Features:EnableDataPartitions` to `true` through your local [appsettings.json](../../src/Microsoft.Health.Dicom.Web/appsettings.json) file or host-specific options. - -Once enabled, the feature modifies the API surface of the DICOM server, and makes any previous data accessible under the `Microsoft.Default` partition. - -> *The data partition feature **cannot be disabled** if partitions other than `Microsoft.Default` are present - a `DataPartitionsFeatureCannotBeDisabledException` will be thrown at startup.* - -## API Changes -All following URIs assume an implicit DICOM service base URI. For example, the base URI of a DICOM server running locally would be `https://localhost:63838/`. -Example requests can be sent in the [Postman collection](../resources/Conformance-as-Postman.postman_collection.json) by providing a value for the `partitionName` collection variable. - -### List Partitions -Lists all data partitions. - -```http -GET /partitions -``` - -#### Request Header - -| Name | Required | Type | Description | -| ------------ | --------- | ------ | ------------------------------- | -| Content-Type | False | string | `application/json` is supported | - -#### Responses - -| Name | Type | Description | -| ----------------- | ----------------------------- | ------------------------------------- | -| 200 (OK) | [Partition](#partition)`[]` | A list of partitions is returned | -| 204 (No Content) | | No partitions exist | -| 400 (Bad Request) | | Data partitions feature is disabled | - -### STOW, WADO, QIDO, and Delete -Once partitions are enabled, STOW, WADO, QIDO, and Delete requests **must** include a data partition URI segment after the base URI, of the form `/partitions/{partitionName}`, where `partitionName` is: - - Up to 64 characters long - - Composed of any combination of alphanumeric characters, `.`, `-`, and `_`, to allow both DICOM UID and GUID formats, as well as human-readable identifiers - -| Action | Example URI | -| ------- | ------------------------------------------------------------------- | -| STOW | `POST /partitions/myPartition-1/studies` | -| WADO | `GET /partitions/myPartition-1/studies/2.25.0000` | -| QIDO | `GET /partitions/myPartition1/studies?StudyInstanceUID=2.25.0000` | -| Delete | `DELETE /partitions/myPartition1/studies/2.25.0000` | - -#### New Responses - -| Name | Message | -| ----------------- | --------------------------------------------------------- | -| 400 (Bad Request) | Data partitions feature is disabled | -| 400 (Bad Request) | PartitionName value is missing in the route segment. | -| 400 (Bad Request) | Specified PartitionName {PartitionName} does not exist. | - -### Other APIs -All other APIs (including [extended query tags](../how-to-guides/extended-query-tags.md), [operations](../how-to-guides/extended-query-tags.md#get-operation), and [change feed](change-feed.md)) will continue to be accessed at the base URI. - -## Managing Partitions - -Currently, the only management operation supported for partitions is an **implicit** creation during STOW requests. -If the partition specified in the URI does not exist, it will be created implicitly and the response will return a retrieve URI -including the partition path. - -## Limitations - - If partitions other than `Microsoft.Default` are present, the feature cannot be disabled - - Querying across partitions is not supported - - Updating and deleting partitions is not supported - -## Definitions - -### Partition -A unit of logical isolation and data uniqueness. - -| Name | Type | Description | -| ------------- | ------ | -------------------------------------------------------------------------------- | -| PartitionKey | int | System-assigned identifier | -| PartitionName | string | Client-assigned unique name, up to 64 alphanumeric characters, `.`, `-`, or `_` | -| CreatedDate | string | The date and time when the partition was created | diff --git a/docs/concepts/dicom-cast.md b/docs/concepts/dicom-cast.md deleted file mode 100644 index 6611ea7fc4..0000000000 --- a/docs/concepts/dicom-cast.md +++ /dev/null @@ -1,84 +0,0 @@ -# DICOM Cast overview - -DICOM Cast allows synchronizing the data from a Medical Imaging Server for DICOM to a [FHIR Server for Azure](https://github.com/microsoft/fhir-server), which allows healthcare organization to integrate clinical and imaging data. DICOM Cast expands the use cases for health data by supporting both a streamlined view of longitudinal patient data and the ability to effectively create cohorts for medical studies, analytics, and machine learning. - -## Architecture - -![Architecture](/docs/images/dicom-cast-architecture.png) - -1. **Poll for batch of changes**: DICOM Cast polls for any changes via [Change Feed](../concepts/change-feed.md), which captures any changes that occur in your Medical Imaging Server for DICOM. -1. **Fetch corresponding FHIR resources, if any**: If any changes correspond to FHIR resources, DICOM Cast will fetch these changes. DICOM Cast synchronizes DICOM tags to the FHIR resource types *Patient* and *ImagingStudy*. -1. **Merge FHIR resources and PUT as a bundle in a transaction**: The FHIR resources corresponding the DICOM Cast captured changes will be merged. The FHIR resources will be PUT as a bundle in a transaction into your Azure API for FHIR server. -1. **Persist state and process next batch**: DICOM Cast will then persist the current state to prepare for next batch of changes. - -The current implementation of DICOM Cast supports: - -- A single-threaded process that reads from DICOM change feed and writes to FHIR server. -- The process is hosted by Azure Container Instance in our sample template, but can be run elsewhere. -- Synchronizes DICOM tags to *Patient* and *ImagingStudy* FHIR resource types*. -- Configuration to ignore invalid tags when syncing data from the change feed to FHIR resource types. - - If `EnforceValidationOfTagValues` is enabled then the change feed entry will not be written to the FHIR server unless every tag that is mapped (see below for mappings) is valid - - If `EnforceValidationOfTagValues` is disabled (default) then as if a value is invalid, but not required to be mapped (see below for required tags for Patient and Imaging Study) then that particular tag will not be mapped but the rest of the change feed entry will be mapped to FHIR resources. If a required tag is invalid then the change feed entry will not be written to FHIR Server -- Configuration to ignore Json parsing errors from DicomWebClient due to malformed DICOM json - - If `IgnoreJsonParsingErrors` is enabled, then the malformed changefeed entry will be skipped. This error will NOT be logged in the exceptions table due to not being able to parse out the Study, Series, and Instance UID necessary for an entry to the table - - If `IgnoreJsonParsingErrors` is disabled, an exception will be thrown when DICOM Cast tries to handle the malformed DICOM json -- Storage of errors to Azure Table Storage - - Errors when processing change feed entries are persisted in Azure Table Storage in different tables depending on the cause of the error. - - `InvalidDicomTagExceptionTable`: Stores information about any tags that had invalid values. Entries in here does not necessarily mean that the entire change feed entry was not stored in FHIR, but that the particular value had a validation issue. - - `DicomFailToStoreExceptionTable`: Stores information about change feed entries that were not stored to FHIR due to an issue with the change feed entry (such as invalid required tag). All entries in this table were not stored to FHIR. - - `FhirFailToStoreExceptionTable`: Stores information about change feed entries that were not stored to FHIR due to an issue with the FHIR server (such as conflicting resource already existing). All entries in this table were not stored to FHIR. - - `TransientRetryExceptionTable`: Stores information about change feed entries that faced a transient error (such as FHIR server too busy) and are being retried. Entries in this table note how many times they have been retried but does not necessarily mean that they eventually failed or succeeded to store to FHIR. - - `TransientFailureExceptionTable`: Stores information about change feed entries that had a transient error, and went through the retry policy and still failed to store to FHIR. All entries in this table failed to store to FHIR. - -## Mappings - -The current implementation of DICOM Cast has the following mappings: - -**Patient:** - -| Property | Tag Id | Tag Name | Required Tag?| Note | -| :------- | :----- | :------- | :----- | :----- | -| Patient.identifier.where(system = 'system') | (0010,0020) | PatientID | Yes | Patient system id will set to the value of patientSystemId configuration or Issuer of Patient Id Dicom tag (0010, 0021) based on the isIssuerIdUsed boolean setting. An empty string will be set by default if the variables are not defined. | -| Patient.name.where(use = 'usual') | (0010,0010) | PatientName | No | PatientName will be split into components and added as HumanName to the Patient resource. | -| Patient.gender | (0010,0040) | PatientSex | No | | -| Patient.birthDate | (0010,0030) | PatientBirthDate | No | PatientBirthDate only contains the date. This implementation assumes that the FHIR and DICOM servers have data from the same time zone. | - -**Endpoint:** - -| Property | Tag Id | Tag Name | Note | -| :------- | :----- | :------- | :--- | -| Endpoint.status ||| The value 'active' will be used when creating Endpoint. | -| Endpoint.connectionType ||| The system 'http://terminology.hl7.org/CodeSystem/endpoint-connection-type' and value 'dicom-wado-rs' will be used when creating Endpoint. | -| Endpoint.address ||| The root URL to the DICOMWeb service will be used when creating Endpoint. The rule is described in 'http://hl7.org/fhir/imagingstudy.html#endpoint' | - -**ImagingStudy:** - -| Property | Tag Id | Tag Name | Required | Note | -| :------- | :----- | :------- | :--- | :--- | -| ImagingStudy.identifier.where(system = 'urn:dicom:uid') | (0020,000D) | StudyInstanceUID | Yes | The value will have prefix of `urn:oid:`. | -| ImagingStudy.status | | | No | The value 'available' will be used when creating ImagingStudy. | -| ImagingStudy.modality | (0008,0060) | Modality | No | Or should this be (0008,0061) ModalitiesInStudy? | -| ImagingStudy.subject | | | No | It will be linked to the Patient [above](##Mappings). | -| ImagingStudy.started | (0008,0020), (0008,0030), (0008,0201) | StudyDate, StudyTime, TimezoneOffsetFromUTC | No | More detail about how timestamp is constructed [below](###Timestamp). | -| ImagingStudy.endpoint | | | | It will be linked to the Endpoint above. | -| ImagingStudy.note | (0008,1030) | StudyDescription | No | | -| ImagingStudy.series.uid | (0020,000E) | SeriesInstanceUID | Yes | | -| ImagingStudy.series.number | (0020,0011) | SeriesNumber | No | | -| ImagingStudy.series.modality | (0008,0060) | Modality | Yes | | -| ImagingStudy.series.description | (0008,103E) | SeriesDescription | No | | -| ImagingStudy.series.started | (0008,0021), (0008,0031), (0008,0201) | SeriesDate, SeriesTime, TimezoneOffsetFromUTC | No | More detail about how timestamp is constructed [below](###Timestamp). | -| ImagingStudy.series.instance.uid | (0008,0018) | SOPInstanceUID | Yes | | -| ImagingStudy.series.instance.sopClass | (0008,0016) | SOPClassUID | Yes | | -| ImagingStudy.series.instance.number | (0020,0013) | InstanceNumber | No| | -| ImagingStudy.identifier.where(type.coding.system='http://terminology.hl7.org/CodeSystem/v2-0203' and type.coding.code='ACSN')) | (0008,0050) | Accession Number | No | Refer to http://hl7.org/fhir/imagingstudy.html#notes. | - -### Timestamp - -DICOM has different date time VR types. Some tags (like Study and Series) have the date, time, and UTC offset stored separately. This means that the date might be partial. This code attempts to translate this into a partial date syntax allowed by the FHIR server. - -## Summary - -In this concept, we reviewed the architecture and mappings of DICOM Cast. To implement DICOM Cast in your Medical Imaging for DICOM Server, refer to the following documents: - -- [Quickstart on DICOM Cast](../quickstarts/deploy-dicom-cast.md) -- [Sync DICOM Metadata to FHIR](../how-to-guides/sync-dicom-metadata-to-fhir.md) diff --git a/docs/concepts/dicom.md b/docs/concepts/dicom.md deleted file mode 100644 index 66cb6b9f5d..0000000000 --- a/docs/concepts/dicom.md +++ /dev/null @@ -1,45 +0,0 @@ -# Medical Imaging Server for DICOM Overview - -## Medical Imaging - -Medical imaging is the technique and process of creating visual representations of the interior of a body for clinical analysis and medical intervention, as well as visual representation of the function of some organs or tissues (physiology). Medical imaging seeks to reveal internal structures hidden by the skin and bones, as well as to diagnose and treat disease. Medical imaging also establishes a database of normal anatomy and physiology to make it possible to identify abnormalities. Although imaging of removed organs and tissues can be performed for medical reasons, such procedures are usually considered part of pathology instead of medical imaging. [Wikipedia, 2020](https://en.wikipedia.org/wiki/Medical_imaging) - -## DICOM - -DICOM (Digital Imaging and Communications in Medicine) is the international standard to transmit, store, retrieve, print, process, and display medical imaging information, and is the primary medical imaging standard accepted across healthcare. Although some exceptions exist (dentistry, veterinary), nearly all medical specialties, equipment manufacturers, software vendors and individual practitioners rely on DICOM at some stage of any medical workflow involving imaging. DICOM ensures that medical images meet quality standards, so that the accuracy of diagnosis can be preserved. Most imaging modalities, including CT, MRI and ultrasound must conform to the DICOM standards. Images that are in the DICOM format need to be accessed and used through specialized DICOM applications. - -## Medical Imaging Server for DICOM - -The Medical Imaging Server for DICOM is an open source DICOM server that is easily deployed on Azure. The Medical Imaging Server for DICOM injects DICOM metadata into the [Azure API for FHIR service](https://docs.microsoft.com/azure/healthcare-apis/), allowing a single source of truth for both clinical data and imaging metadata. It allows standards-based communication with any DICOMweb™ enabled systems. - -The need to effectively integrate non-clinical data has become acute. In order to effectively treat patients, research new treatments or diagnostic solutions or simply provide an effective overview of the health history of a single patient, organizations must integrate data across several sources. One of the most pressing integrations is between clinical and imaging data. - -FHIR™ is becoming an important standard for clinical data and provides extensibility to support integration of other types of data directly, or through references. By using the Medical Imaging Server for DICOM, organizations can store references to imaging data in FHIR™ and enable queries that cross clinical and imaging datasets. This can enable many different scenarios, for example: - -- **Creating cohorts for research.** Often through queries for patients that match data in both clinical and imaging systems, such as this one (which triggered the effort to integrate FHIR™ and DICOM data): “Give me all the medications prescribed with all the CT Scan documents and their associated radiology reports for any patient older than 45 that has had a diagnosis of osteosarcoma over the last 2 years.” -- **Finding outcomes for similar patients to understand options and plan treatments.** When presented with a patient diagnosis, a physician can identify patient outcomes and treatment plans for past patients with a similar diagnosis, even when these include imaging data. -- **Providing a longitudinal view of a patient during diagnosis.** Radiologists, especially teleradiologists, often do not have complete access to a patient’s medical history and related imaging studies. Through FHIR™ integration, this data can be easily provided, even to radiologists outside of the organization’s local network. -- **Closing the feedback loop with teleradiologists.** Ideally a radiologist has access to a hospital’s clinical data to close the feedback loop after making a recommendation. However, for teleradiologists this is often not the case. Instead, they are often unable to close the feedback loop after performing a diagnosis, since they do not have access to patient data after the initial read. With no (or limited) access to clinical results or outcomes, they cannot get the feedback necessary to improve their skills. As on teleradiologist put it: “Take parathyroid for example. We do more than any other clinic in the country, and yet I have to beg and plead for surgeons to tell me what they actually found. Out of the more than 500 studies I do each month, I get direct feedback on only three or four.” Through integration with FHIR™, an organization can easily create a tool that will provide direct feedback to teleradiologists, helping them to hone their skills and make better recommendations in the future. -- **Closing the feedback loop for AI/ML models.** Machine learning models do best when real-world feedback can be used to improve their models. However, 3rd party ML model providers rarely get the feedback they need to improve their models over time. For instance, one ISV put it this way: “We us a combination of machine models and human experts to recommend a treatment plan for heart surgery. However, we only rarely get feedback from physicians on how accurate our plan was. For instance, we often recommend a stent size. We’d love to get feedback on if our prediction was correct, but the only time we hear from customers is when there’s a major issue with our recommendations.” As with feedback for teleradiologists, integration with FHIR™ allows organizations to create a mechanism to provide feedback to the model retraining pipeline. - -## Deployment of Medical Imaging Server for DICOM To Azure - -The Medical Imaging Server for DICOM needs an Azure subscription to configure and run the required components. These components are, by default, created inside of an existing or new Azure Resource Group to simplify management. Additionally, an Azure Active Directory account is required. The diagram below depicts all of the resources created within your resource group. - -![resource-deployment](../images/dicom-deployment-architecture.png) - -- **Azure SQL**: Indexes a subset of the Medical Imaging Server for DICOM metadata to support queries and to maintain a queryable log of changes. -- **App Service Plan**: Hosts the Medical Imaging Server for DICOM. -- **Azure Key Vault**: Stores critical security information. -- **Storage Account**: Blob Storage which persists all Medical Imaging Server for DICOM data and metadata. -- **Application Insights** (optional): Monitors performance of Medical Imaging Server for DICOM. -- **Azure Container Instance** (optional): Hosts the DICOM Cast service for Azure API for FHIR integration. -- **Azure API for FHIR** (optional): Persists the DICOM metadata alongside other clinical data. - -## Summary - -This Concept provided an overview of DICOM, Medical Imaging and the Medical Imaging Server for DICOM. To get started using the Medical Imaging Server: - -- [Deploy Medical Imaging Server to Azure](../quickstarts/deploy-via-azure.md) -- [Deploy DICOM Cast](../quickstarts/deploy-dicom-cast.md) -- [Use the Medical Imaging Server for DICOM APIs](../tutorials/use-the-medical-imaging-server-apis.md) diff --git a/docs/concepts/dicomevents.md b/docs/concepts/dicomevents.md deleted file mode 100644 index 97e41ed190..0000000000 --- a/docs/concepts/dicomevents.md +++ /dev/null @@ -1,153 +0,0 @@ -# DICOM Events - -Events are a notification and subscription feature in the Azure Health Data Services. Events enable customers to utilize and enhance the analysis and workflows of Digital Imaging and Communications in Medicine (DICOM) images. When DICOM image changes are successfully written to the Azure Health Data Services, the Events feature sends notification messages to Events subscribers. These event notification occurrences can be sent to multiple endpoints to trigger automation ranging from starting workflows to sending email and text messages to support the changes occurring from the health data it originated from. The Events feature integrates with the Azure Event Grid service and creates a system topic for the Azure Health Data Services Workspace. - -## Event types: - -DicomImageCreated - The event emitted after a DICOM image gets created successfully. - -DicomImageDeleted - The event emitted after a DICOM image gets deleted successfully. - -## Event message structure: - -|Name | Type | Required | Description -|-----|------|----------|-----------| -|topic | string | Yes | The topic is the Azure Resource ID of your Azure Health Data Services workspace. -|subject | string | Yes | The Uniform Resource Identifier (URI) of the DICOM image that was changed. Customer can access the image with the subject with https:// scheme. Customer should use the dataVersion or data.resourceVersionId to visit specific data version regarding this event. -| eventType | string(enum) | Yes | The type of change on the DICOM image. -| eventTime | string(datetime) | Yes | The UTC time when the DICOM image change was committed. -| id | string | Yes | Unique identifier for the event. -| data | object | Yes | DICOM image change event details. -| data.imageStudyInstanceUid | string | Yes | The image's Study Instance UID -| data.imageSeriesInstanceUid | string | Yes | The image's Series Instance UID -| data.imageSopInstanceUid | string | Yes | The image's SOP Instance UID -| data.serviceHostName | string | Yes | The hostname of the dicom service where the change occurred. -| data.sequenceNumber | int | Yes | The sequence number of the change in the DICOM service. Every image creation and deletion will have a unique sequence within the service. This number correlates to the sequence number of the DICOM service's Change Feed. Querying the DICOM Service Change Feed with this sequence number will give you the change that created this event. -| dataVersion | string | No | The data version of the DICOM image -| metadataVersion | string | No | The schema version of the event metadata. This is defined by Azure Event Grid and should be constant most of the time. - -## Samples - -## Microsoft.HealthcareApis.DicomImageCreated - -### Event Grid Schema - -``` -{ - "id": "d621839d-958b-4142-a638-bb966b4f7dfd", - "topic": "/subscriptions/{subscription-id}/resourceGroups/{resource-group-name}/providers/Microsoft.HealthcareApis/workspaces/{workspace-name}", - "subject": "{dicom-account}.dicom.azurehealthcareapis.com/v1/studies/1.2.3.4.3/series/1.2.3.4.3.9423673/instances/1.3.6.1.4.1.45096.2.296485376.2210.1633373143.864442", - "data": { - "imageStudyInstanceUid": "1.2.3.4.3", - "imageSeriesInstanceUid": "1.2.3.4.3.9423673", - "imageSopInstanceUid": "1.3.6.1.4.1.45096.2.296485376.2210.1633373143.864442", - "serviceHostName": "{dicom-account}.dicom.azurehealthcareapis.com", - "sequenceNumber": 1 - }, - "eventType": "Microsoft.HealthcareApis.DicomImageCreated", - "dataVersion": "1", - "metadataVersion": "1", - "eventTime": "2022-09-15T01:14:04.5613214Z" -} -``` - -### Cloud Events Schema - -``` -{ - "source": "/subscriptions/{subscription-id}/resourceGroups/{resource-group-name}/providers/Microsoft.HealthcareApis/workspaces/{workspace-name}", - "subject": "{dicom-account}.dicom.azurehealthcareapis.com/v1/studies/1.2.3.4.3/series/1.2.3.4.3.9423673/instances/1.3.6.1.4.1.45096.2.296485376.2210.1633373143.864442", - "type": "Microsoft.HealthcareApis.DicomImageCreated", - "time": "2022-09-15T01:14:04.5613214Z", - "id": "d621839d-958b-4142-a638-bb966b4f7dfd", - "data": { - "imageStudyInstanceUid": "1.2.3.4.3", - "imageSeriesInstanceUid": "1.2.3.4.3.9423673", - "imageSopInstanceUid": "1.3.6.1.4.1.45096.2.296485376.2210.1633373143.864442", - "serviceHostName": "{dicom-account}.dicom.azurehealthcareapis.com", - "sequenceNumber": 1 - }, - "specVersion": "1.0" -} -``` - -## Microsoft.HealthcareApis.DicomImageDeleted - -### Event Grid Schema - -``` -{ - "id": "eac1c1a0-ffa8-4b28-97cc-1d8b9a0a6021", - "topic": "/subscriptions/{subscription-id}/resourceGroups/{resource-group-name}/providers/Microsoft.HealthcareApis/workspaces/{workspace-name}", - "subject": "{dicom-account}.dicom.azurehealthcareapis.com/v1/studies/1.2.3.4.3/series/1.2.3.4.3.9423673/instances/1.3.6.1.4.1.45096.2.296485376.2210.1633373143.864442", - "data": { - "imageStudyInstanceUid": "1.2.3.4.3", - "imageSeriesInstanceUid": "1.2.3.4.3.9423673", - "imageSopInstanceUid": "1.3.6.1.4.1.45096.2.296485376.2210.1633373143.864442", - "serviceHostName": "{dicom-account}.dicom.azurehealthcareapis.com", - "sequenceNumber": 2 - }, - "eventType": "Microsoft.HealthcareApis.DicomImageDeleted", - "dataVersion": "1", - "metadataVersion": "1", - "eventTime": "2022-09-15T01:16:07.5692209Z" -} -``` - -### Cloud Events Schema - -``` -{ - "source": "/subscriptions/{subscription-id}/resourceGroups/{resource-group-name}/providers/Microsoft.HealthcareApis/workspaces/{workspace-name}", - "subject": "{dicom-account}.dicom.azurehealthcareapis.com/v1/studies/1.2.3.4.3/series/1.2.3.4.3.9423673/instances/1.3.6.1.4.1.45096.2.296485376.2210.1633373143.864442", - "type": "Microsoft.HealthcareApis.DicomImageDeleted", - "time": "2022-09-15T01:14:04.5613214Z", - "id": "eac1c1a0-ffa8-4b28-97cc-1d8b9a0a6021", - "data": { - "imageStudyInstanceUid": "1.2.3.4.3", - "imageSeriesInstanceUid": "1.2.3.4.3.9423673", - "imageSopInstanceUid": "1.3.6.1.4.1.45096.2.296485376.2210.1633373143.864442", - "serviceHostName": "{dicom-account}.dicom.azurehealthcareapis.com", - "sequenceNumber": 2 - }, - "specVersion": "1.0" -} -``` - -## FAQs - -### Can I use Events with a different DICOM service other than the Azure Health Data Services DICOM service? -No. The Azure Health Data Services Events feature only currently supports the Azure Health Data Services DICOM service. - -### What DICOM image events does Events support? -Events are generated from the following DICOM service types: - -DicomImageCreated - The event emitted after a DICOM image gets created successfully. - -DicomImageDeleted - The event emitted after a DICOM image gets deleted successfully. - -### What is the payload of an Events message? -For a detailed description of the Events message structure and both required and non-required elements, see the `Event message structure` section. - -### What is the throughput for the Events messages? -The throughput of DICOM events is governed by the throughput of the DICOM service and the Event Grid. When a request made to the DICOM service is successful, it will return a 2xx HTTP status code. It will also generate a DICOM image changing event. The current limitation is 5,000 events/second per a workspace for all DICOM service instances in it. - -### How am I charged for using Events? -There are no extra charges for using Azure Health Data Services Events. However, applicable charges for the Event Grid will be assessed against your Azure subscription. - -### How do I subscribe to multiple DICOM services in the same workspace separately? -You can use the Event Grid filtering feature. There are unique identifiers in the event message payload to differentiate different accounts and workspaces. You can find a global unique identifier for workspace in the source field, which is the Azure Resource ID. You can locate the unique DICOM account name in that workspace in the `data.serviceHostName` field. When you create a subscription, you can use the filtering operators to select the events you want to get in that subscription. - -### Can I use the same subscriber for multiple workspaces or multiple DICOM accounts? -Yes. We recommend that you use different subscribers for each individual DICOM account to process in isolated scopes. - -### Is Event Grid compatible with HIPAA and HITRUST compliance obligations? -Yes. Event Grid supports customer's Health Insurance Portability and Accountability Act (HIPAA) and Health Information Trust Alliance (HITRUST) obligations. For more information, see Microsoft Azure Compliance Offerings. - -### What is the expected time to receive an Events message? -On average, you should receive your event message within ten seconds after a successful HTTP request. 99.99% of the event messages should be delivered within twenty seconds unless the limitation of either the DICOM service or Event Grid has been met. - -### Is it possible to receive duplicate Events message? -Yes. The Event Grid guarantees at least one Events message delivery with its push mode. There may be chances that the event delivery request returns with a transient failure status code for random reasons. In this situation, the Event Grid will consider that as a delivery failure and will resend the Events message. For more information, see Azure Event Grid delivery and retry. - -Generally, we recommend that developers ensure idempotency for the event subscriber. The event ID or the combination of all fields in the data property of the message content are unique per each event. The developer can rely on them to de-duplicate. diff --git a/docs/concepts/extended-query-tags.md b/docs/concepts/extended-query-tags.md deleted file mode 100644 index af847682a7..0000000000 --- a/docs/concepts/extended-query-tags.md +++ /dev/null @@ -1,472 +0,0 @@ -# Extended Query Tags - -## Overview - -By default, the Medical Imaging Server for DICOM supports querying on the DICOM tags specified in the [conformance statement](https://github.com/microsoft/dicom-server/blob/main/docs/resources/conformance-statement.md#searchable-attributes). However, this list of tags may be expanded by enabling _extended query tags_. Using the APIs listed below, users can additionally index their DICOM studies, series, and instances on both standard and private DICOM tags such that they can be specified in QIDO-RS. - - - -## APIs - -### Version: v1-prerelease, v1 - -To help manage the supported tags in a given DICOM server instance, the following API endpoints have been added. - -| API | Description | -| ------------------------------------------------- | ------------------------------------------------------------ | -| POST .../extendedquerytags | [Add Extended Query Tags](#add-extended-query-tags) | -| GET .../extendedquerytags | [List Extended Query Tags](#list-extended-query-tags) | -| GET .../extendedquerytags/{tagPath} | [Get Extended Query Tag](#get-extended-query-tag) | -| DELETE .../extendedquerytags/{tagPath} | [Delete Extended Query Tag](#delete-extended-query-tag) | -| PATCH .../extendedquerytags/{tagPath} | [Update Extended Query Tag](#update-extended-query-tag) | -| GET .../extendedquerytags/{tagPath}/errors | [List Extended Query Tag Errors](#list-extended-query-tag-errors) | -| GET .../operations/{operationId} | [Get Operation](#get-operation) | - -### Add Extended Query Tags - -Add one or more extended query tags and starts a long-running operation that re-indexes current DICOM instances on the specified tag(s). - -```http -POST .../extendedquerytags -``` - -#### Request Header - -| Name | Required | Type | Description | -| ------------ | -------- | ------ | ------------------------------- | -| Content-Type | True | string | `application/json` is supported | - -#### Request Body - -| Name | Required | Type | Description | -| ---- | -------- | ------------------------------------------------------------ | ----------- | -| body | | [Extended Query Tag for Adding](#extended-query-tag-for-adding)`[]` | | - -#### Limitations - -The following VR types are supported: - -| VR | Description | Single Value Matching | Range Matching | Fuzzy Matching | -| ---- | --------------------- | --------------------- | -------------- | -------------- | -| AE | Application Entity | X | | | -| AS | Age String | X | | | -| CS | Code String | X | | | -| DA | Date | X | X | | -| DS | Decimal String | X | | | -| DT | Date Time | X | X | | -| FD | Floating Point Double | X | | | -| FL | Floating Point Single | X | | | -| IS | Integer String | X | | | -| LO | Long String | X | | | -| PN | Person Name | X | | X | -| SH | Short String | X | | | -| SL | Signed Long | X | | | -| SS | Signed Short | X | | | -| TM | Time | X | X | | -| UI | Unique Identifier | X | | | -| UL | Unsigned Long | X | | | -| US | Unsigned Short | X | | | - -> Sequential tags i.e. tags under a tag of type Sequence of Items (SQ) are currently not supported. - -> You can add up to 128 extended query tags. - -> Only the first value will be indexed of a single valued data element that incorrectly has multiple values. - -> We do not index extended query tags if the value is null or empty. - -#### Responses - -| Name | Type | Description | -| ----------------- | ------------------------------------------- | ------------------------------------------------------------ | -| 202 (Accepted) | [Operation Reference](#operation-reference) | Extended query tag(s) have been added, and a long-running operation has been started to re-index existing DICOM instances | -| 400 (Bad Request) | | Request body has invalid data | -| 409 (Conflict) | | One or more requested query tags already are supported | - -### List Extended Query Tags - -Lists of all extended query tag(s). - -```http -GET .../extendedquerytags -``` - -#### Responses - -| Name | Type | Description | -| -------- | --------------------------------------------- | --------------------------- | -| 200 (OK) | [Extended Query Tag](#extended-query-tag)`[]` | Returns extended query tags | - -### Get Extended Query Tag - -Get an extended query tag. - -```http -GET .../extendedquerytags/{tagPath} -``` - -#### URI Parameters - -| Name | In | Required | Type | Description | -| ------- | ---- | -------- | ------ | ------------------------------------------------------------ | -| tagPath | path | True | string | tagPath is the path for the tag, which can be either tag or keyword. E.g. Patient Id is represented by `00100020` or `PatientId` | - -#### Responses - -| Name | Type | Description | -| ----------------- | ----------------------------------------- | ------------------------------------------------------ | -| 200 (OK) | [Extended Query Tag](#extended-query-tag) | The extended query tag with the specified `tagPath` | -| 400 (Bad Request) | | Requested tag path is invalid | -| 404 (Not Found) | | Extended query tag with requested tagPath is not found | - -### Delete Extended Query Tag - -Delete an extended query tag. - -```http -DELETE .../extendedquerytags/{tagPath} -``` - -#### URI Parameters - -| Name | In | Required | Type | Description | -| ------- | ---- | -------- | ------ | ------------------------------------------------------------ | -| tagPath | path | True | string | tagPath is the path for the tag, which can be either tag or keyword. E.g. Patient Id is represented by `00100020` or `PatientId` | - -#### Responses - -| Name | Type | Description | -| ----------------- | ---- | ------------------------------------------------------------ | -| 204 (No Content) | | Extended query tag with requested tagPath has been successfully deleted. | -| 400 (Bad Request) | | Requested tag path is invalid. | -| 404 (Not Found) | | Extended query tag with requested tagPath is not found | - -### Update Extended Query Tag - -Update an extended query tag. - -```http -PATCH .../extendedquerytags/{tagPath} -``` - -#### URI Parameters - -| Name | In | Required | Type | Description | -| ------- | ---- | -------- | ------ | ------------------------------------------------------------ | -| tagPath | path | True | string | tagPath is the path for the tag, which can be either tag or keyword. E.g. Patient Id is represented by `00100020` or `PatientId` | - -#### Request Header - -| Name | Required | Type | Description | -| ------------ | -------- | ------ | -------------------------------- | -| Content-Type | True | string | `application/json` is supported. | - -#### Request Body - -| Name | Required | Type | Description | -| ---- | -------- | ------------------------------------------------------------ | ----------- | -| body | | [Extended Query Tag for Updating](#extended-query-tag-for-updating) | | - -#### Responses - -| Name | Type | Description | -| ----------------- | ----------------------------------------- | ------------------------------------------------------ | -| 20 (OK) | [Extended Query Tag](#extended-query-tag) | The updated extended query tag | -| 400 (Bad Request) | | Requested tag path or body is invalid | -| 404 (Not Found) | | Extended query tag with requested tagPath is not found | - -### List Extended Query Tag Errors - -Lists errors on an extended query tag. - -```http -GET .../extendedquerytags/{tagPath}/errors -``` - -#### URI Parameters - -| Name | In | Required | Type | Description | -| ------- | ---- | -------- | ------ | ------------------------------------------------------------ | -| tagPath | path | True | string | tagPath is the path for the tag, which can be either tag or keyword. E.g. Patient Id is represented by `00100020` or `PatientId` | - -#### Responses - -| Name | Type | Description | -| ----------------- | ---------------------------------------------------------- | --------------------------------------------------------- | -| 200 (OK) | [Extended Query Tag Error](#extended-query-tag-error) `[]` | List of extended query tag errors associated with the tag | -| 400 (Bad Request) | | Requested tag path is invalid | -| 404 (Not Found) | | Extended query tag with requested tagPath is not found | - -### Get Operation - -Get a long-running operation. - -```http -GET .../operations/{operationId} -``` - -#### URI Parameters - -| Name | In | Required | Type | Description | -| ----------- | ---- | -------- | ------ | ---------------- | -| operationId | path | True | string | The operation id | - -#### Responses - -| Name | Type | Description | -| --------------- | ----------------------- | -------------------------------------------- | -| 200 (OK) | [Operation](#operation) | The completed operation for the specified ID | -| 202 (Accepted) | [Operation](#operation) | The running operation for the specified ID | -| 404 (Not Found) | | The operation is not found | - -## QIDO with Extended Query Tags - -### Tag Status - -The [Status](#extended-query-tag-status) of Extended query tag indicates current status. When an extended query tag is first added, its status is set to `Adding`, and a long-running operation is kicked off to reindex existing DICOM instances. After the operation is completed, the tag status is updated to `Ready`. The extended query tag can now be used in [QIDO](../resources/conformance-statement.md#search-qido-rs). - -For example, if the tag Manufacturer Model Name (0008,1090) is added, and in `Ready` status, hereafter the following queries can be used to filter stored instances by Manufacturer Model Name: - -```http -../instances?ManufacturerModelName=Microsoft -``` - -They can also be used in conjunction with existing tags. E.g: - -```http -../instances?00081090=Microsoft&PatientName=Jo&fuzzyMatching=true -``` - -> After extended query tag is added, any DICOM instance stored is indexed on it - -### Tag Query Status - -[QueryStatus](#extended-query-tag-status) indicates whether QIDO is allowed for the tag. When a reindex operation fails to process one or more DICOM instances for a tag, that tag's QueryStatus is set to `Disabled` automatically. You can choose to ignore indexing errors and allow queries to use this tag by setting the `QueryStatus` to `Enabled` via [Update Extended Query Tag](#update-extended-query-tag) API. Any QIDO requests that reference at least one manually enabled tag will include the set of tags with indexing errors in the response header `erroneous-dicom-attributes`. - -For example, suppose the extended query tag `PatientAge` had errors during reindexing, but was enabled manually. For the query below, you would be able to see `PatientAge` in the `erroneous-dicom-attributes` header. - -```http -../instances?PatientAge=035Y -``` - -## Definitions - -### Extended Query Tag - -A non-standard DICOM tag that will be supported for QIDO-RS. - -| Name | Type | Description | -| -------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | -| Path | string | Path of tag, normally composed of group id and element id. E.g. `PatientId` (0010,0020) has path 00100020 | -| VR | string | Value representation of this tag | -| PrivateCreator | string | Identification code of the implementer of this private tag | -| Level | [Extended Query Tag Level](#extended-query-tag-level) | Level of extended query tag | -| Status | [Extended Query Tag Status](#extended-query-tag-status) | Status of the extended query tag | -| QueryStatus | [Extended Query Tag Query Status](#extended-query-tag-query-status) | Query status of extended query tag | -| Errors | [Extended Query Tag Errors Reference](#extended-query-tag-errors-reference) | Reference to extended query tag errors | -| Operation | [Operation Reference](#operation-reference) | Reference to a long-running operation | - -**Example1:** a standard tag (0008,0070) in `Ready` status. - -```json -{ - "status": "Ready", - "level": "Instance", - "queryStatus": "Enabled", - "path": "00080070", - "vr": "LO" -} -``` - -**Example2:** a standard tag (0010,1010) in `Adding` status. An operation with id `1a5d0306d9624f699929ee1a59ed57a0` is running on it, and 21 errors has occurred so far. - -```json -{ - "status": "Adding", - "level": "Study", - "errors": { - "count": 21, - "href": "https://localhost:63838/extendedquerytags/00101010/errors" - }, - "operation": { - "id": "1a5d0306d9624f699929ee1a59ed57a0", - "href": "https://localhost:63838/operations/1a5d0306d9624f699929ee1a59ed57a0" - }, - "queryStatus": "Disabled", - "path": "00101010", - "vr": "AS" -} -``` - -### Operation Reference - -Reference to a long-running operation. - -| Name | Type | Description | -| ---- | ------ | -------------------- | -| Id | string | operation id | -| Href | string | Uri to the operation | - -### Operation - -Represents a long-running operation. - -| Name | Type | Description | -| --------------- | ------------------------------------- | ------------------------------------------------------------ | -| OperationId | string | The operation Id | -| OperationType | [Operation Type](#operation-type) | Type of the long running operation | -| CreatedTime | string | Time when the operation was created | -| LastUpdatedTime | string | Time when the operation was updated last time | -| Status | [Operation Status](#operation-status) | Represents run time status of operation | -| PercentComplete | Integer | Percentage of work that has been completed by the operation | -| Resources | string`[]` | Collection of resources locations that the operation is creating or manipulating | - -**Example:** a running reindex operation. - -```json -{ - "resources": [ - "https://localhost:63838/extendedquerytags/00101010" - ], - "operationId": "a99a8b51-78d4-4fd9-b004-b6c0bcaccf1d", - "type": "Reindex", - "createdTime": "2021-10-06T16:40:02.5247083Z", - "lastUpdatedTime": "2021-10-06T16:40:04.5152934Z", - "status": "Running", - "percentComplete": 10 -} -``` - - - -### Operation Status - -Represents run time status of long running operation. - -| Name | Type | Description | -| ---------- | ------ | ------------------------------------------------------------ | -| NotStarted | string | The operation is not started | -| Running | string | The operation is executing and has not yet finished | -| Completed | string | The operation has finished successfully | -| Failed | string | The operation has stopped prematurely after encountering one or more errors | - -### Extended Query Tag Error - -An error that occurred during an extended query tag indexing operation. - -| Name | Type | Description | -| ----------------- | ------ | ------------------------------------------------- | -| StudyInstanceUid | string | Study instance UID where indexing errors occured | -| SeriesInstanceUid | string | Series instance UID where indexing errors occured | -| SopInstanceUid | string | Sop instance UID where indexing errors occured | -| CreatedTime | string | Time when error occured(UTC) | -| ErrorMessage | string | Error message | - -**Example**: an unexpected value length error on an DICOM instance. It occurred at 2021-10-06T16:41:44.4783136. - -```json -{ - "studyInstanceUid": "2.25.253658084841524753870559471415339023884", - "seriesInstanceUid": "2.25.309809095970466602239093351963447277833", - "sopInstanceUid": "2.25.225286918605419873651833906117051809629", - "createdTime": "2021-10-06T16:41:44.4783136", - "errorMessage": "Value length is not expected." -} -``` - -### Extended Query Tag Errors Reference - -Reference to extended query tag errors. - -| Name | Type | Description | -| ----- | ------- | ------------------------------------------------ | -| Count | Integer | Total number of errors on the extended query tag | -| Href | string | Uri to extended query tag errors | - -### Operation Type - -The type of a long-running operation. - -| Name | Type | Description | -| ------- | ------ | ------------------------------------------------------------ | -| Reindex | string | A reindex operation that updates the indices for previously added data based on new tags | - -### Extended Query Tag Status - -The status of extended query tag. - -| Name | Type | Description | -| -------- | ------ | ------------------------------------------------------------ | -| Adding | string | The extended query tag has been added, and a long-running operation is reindexing existing DICOM instances | -| Ready | string | The extended query tag is ready for QIDO-RS | -| Deleting | string | The extended query tag is being deleted | - -### Extended Query Tag Level - -The level of the DICOM information hierarchy where this tag applies. - -| Name | Type | Description | -| -------- | ------ | -------------------------------------------------------- | -| Instance | string | The extended query tag is relevant at the instance level | -| Series | string | The extended query tag is relevant at the series level | -| Study | string | The extended query tag is relevant at the study level | - -### Extended Query Tag Query Status - -The query status of extended query tag. - -| Name | Type | Description | -| -------- | ------ | --------------------------------------------------- | -| Disabled | string | The extended query tag is not allowed to be queried | -| Enabled | string | The extended query tag is allowed to be queried | - -> Note: Errors during reindex operation disables QIDO on the extended query tag. You can call [Update Extended Query Tag](#update-extended-query-tag) API to enable it. - -### Extended Query Tag for Updating - -Represents extended query tag for updating. - -| Name | Type | Description | -| ----------- | ------------------------------------------------------------ | -------------------------------------- | -| QueryStatus | [Extended Query Tag Query Status](#extended-query-tag-query-status) | The query status of extended query tag | - -### Extended Query Tag for Adding - -Represents extended query tag for adding. - -| Name | Required | Type | Description | -| -------------- | -------- | ----------------------------------------------------- | ------------------------------------------------------------ | -| Path | True | string | Path of tag, normally composed of group id and element id. E.g. `PatientId` (0010,0020) has path 00100020 | -| VR | | string | Value representation of this tag. It's optional for standard tag, and required for private tag | -| PrivateCreator | | string | Identification code of the implementer of this private tag. Only set when the tag is a private tag | -| Level | True | [Extended Query Tag Level](#extended-query-tag-level) | Represents the hierarchy at which this tag is relevant. Should be one of Study, Series or Instance | - -**Example1:** `MicrosoftPC` is defining the private tag (0401,1001) with the `SS` value representation on Instance level - -```json -{ - "Path": "04011001", - "VR": "SS", - "PrivateCreator": "MicrosoftPC", - "Level": "Instance" -} -``` - -**Example2:** the standard tag with keyword `ManufacturerModelName` with the `LO` value representation is defined on Series level - -```json -{ - "Path": "ManufacturerModelName", - "VR": "LO", - "Level": "Series" -} -``` - - **Example3:** the standard tag (0010,0040) is defined on studies: the value representation is already defined by the DICOM standard - -```json -{ - "Path": "00100040", - "Level": "Study" -} -``` diff --git a/docs/dcms/blue-circle.dcm b/docs/dcms/blue-circle.dcm deleted file mode 100644 index b2755e82ea..0000000000 Binary files a/docs/dcms/blue-circle.dcm and /dev/null differ diff --git a/docs/dcms/dicom-metadata.csv b/docs/dcms/dicom-metadata.csv deleted file mode 100644 index fd2e68cdf4..0000000000 --- a/docs/dcms/dicom-metadata.csv +++ /dev/null @@ -1,4 +0,0 @@ -SpecificCharacterSet,ImageType,SOPClassUID,SOPInstanceUID,StudyDate,ContentDate,AcquisitionDateTime,StudyTime,ContentTime,AccessionNumber,Modality,ConversionType,ReferringPhysicianName,PatientName,PatientID,PatientBirthDate,PatientSex,PatientAge,PatientPosition,StudyInstanceUID,SeriesInstanceUID,StudyID,SeriesNumber,InstanceNumber,PatientOrientation,Laterality,ImageComments,SamplesPerPixel,PhotometricInterpretation,PlanarConfiguration,Rows,Columns,PixelSpacing,BitsAllocated,BitsStored,HighBit,PixelRepresentation,SmallestImagePixelValue,LargestImagePixelValue,fname,MultiImageType,ImageType1,ImageType2,MultiPixelSpacing,PixelSpacing1 -ISO_IR 192,DERIVED,1.2.840.10008.5.1.4.1.1.7,1.2.826.0.1.3680043.8.498.13273713909719068980354078852867170114,20200922,,,120000,,,OT,SYN,Doctor Doc,Anony Mous,ID1,,F,024Y,,1.2.826.0.1.3680043.8.498.13230779778012324449356534479549187420,1.2.826.0.1.3680043.8.498.77033797676425927098669402985243398207,1,2,1,,,Fake DICOM - Blue Circle,3,RGB,0,979,985,1,8,8,7,0,0,255,C:\data\fakedicom\files\blue-circle.dcm,1,SECONDARY,OTHER,1,1 -ISO_IR 192,DERIVED,1.2.840.10008.5.1.4.1.1.7,1.2.826.0.1.3680043.8.498.12714725698140337137334606354172323212,20200922,,,120000,,,OT,SYN,Doctor Doc,Anony Mous,ID1,,F,024Y,,1.2.826.0.1.3680043.8.498.13230779778012324449356534479549187420,1.2.826.0.1.3680043.8.498.45787841905473114233124723359129632652,1,1,2,,,Fake DICOM - Green Square,3,RGB,0,979,985,1,8,8,7,0,0,255,C:\data\fakedicom\files\green-square.dcm,1,SECONDARY,OTHER,1,1 -ISO_IR 192,DERIVED,1.2.840.10008.5.1.4.1.1.7,1.2.826.0.1.3680043.8.498.47359123102728459884412887463296905395,20200922,,,120000,,,OT,SYN,Doctor Doc,Anony Mous,ID1,,F,024Y,,1.2.826.0.1.3680043.8.498.13230779778012324449356534479549187420,1.2.826.0.1.3680043.8.498.45787841905473114233124723359129632652,1,1,1,,,Fake DICOM - Red Triangle,3,RGB,0,979,985,1,8,8,7,0,0,255,C:\data\fakedicom\files\red-triangle.dcm,1,SECONDARY,OTHER,1,1 diff --git a/docs/dcms/green-square.dcm b/docs/dcms/green-square.dcm deleted file mode 100644 index 61bc03d976..0000000000 Binary files a/docs/dcms/green-square.dcm and /dev/null differ diff --git a/docs/dcms/red-triangle.dcm b/docs/dcms/red-triangle.dcm deleted file mode 100644 index b9cbe05269..0000000000 Binary files a/docs/dcms/red-triangle.dcm and /dev/null differ diff --git a/docs/development/api-versioning-developers.md b/docs/development/api-versioning-developers.md deleted file mode 100644 index ca3b67f62f..0000000000 --- a/docs/development/api-versioning-developers.md +++ /dev/null @@ -1,75 +0,0 @@ -# API Versioning for DICOM Server - Developer Guide - -This guide gives an overview of the API versioning of the REST endpoints for DICOM Server. - -## Routes - -API Version number are set within the route. Example: -`/v1/studies` - -To add a route, use the `[VersionedRoute]` attribute to automatically add the version number to the route. Example: -```C# -[HttpPost] -[VersionedRoute("studies")] -public async Task PostAsync(string studyInstanceUid = null) -``` - -## Incrementing the version - -We will only increment the major version of the API, and leave out the minor version. Ex: 1, 2, 3, etc. - -Our prerelease version included the minor version: `1.0-prerelease`, but we have moved away from that syntax. - -### Breaking change -The major version must be incremented if a breaking change is introduced. - -List of things we will consider to be a breaking change -1. Renaming or removing endpoints -1. Removing parameters or adding mandatory parameters -1. Changing status code -1. Deleting property in response or altering response type at all (but okay to add properties to the response) -1. Changing the type of a property -1. Behavior of an API changes (changes in buisness logic, used to do foo, now does bar) - -More info on breaking changes from the [REST guidelines](https://github.com/Microsoft/api-guidelines/blob/master/Guidelines.md#123-definition-of-a-breaking-change) - -Additive changes are not considered breaking changes. For example, adding a response field or adding a new route. - -Bug fixes are not considered breaking changes. - -### Prerelease versions - -Adding a version with the status "prerelease" is a good idea if you have breaking changes to add that are still prone to change, or are not production ready. -Prerelease versions may experience breaking changes and are not recommended for customers to use in production environments. - -`[ApiVersion("x.0-prerelease")]` - -or - -`ApiVersion prereleaseVersion = new ApiVersion(x, 0, "prerelease");` - -### Testing for breaking changes -Currently we have a test in our pr and ci pipeline that checks to make sure that any defined api versions do not have any breaking changes (changes that are not backward compatible). We use [OpenAPI-diff](https://github.com/OpenAPITools/openapi-diff) to compare a baseline OpenApi Doc for each version with a version that is generated after the build step in the pipeline. If there are breaking changes detected between the baseline that is checked into the repo and the OpenApi doc generated in the pipeline, then the pipeline fails. - -### How to increment the version - -1. Add a new controller to hold the endpoints for the new version, and annotate with `[ApiVersion("")]`. All existing endpoints must get the new version. -2. Add the new version number to `test/Microsoft.Health.Dicom.Api.UnitTests/Features/Routing/UrlResolverTests.cs` to test the new endpoints. -3. Test to verify the breaking changes were not added to the previous version(s). -4. Do the following to add the checks in the pr and ci pipeline to verify that developers do not accidentally create breaking changes. - 1. Add the new version to the arguments in `build/common/versioning.yml`. The powershell script takes in an array of versions so the new version can just be added to the argument. - 1. Generate the yaml file for the new version and save it to `/dicom-server/swagger/{Version}/swagger.yaml`. This will allow us to use this as the new baseline to compare against in the pr and ci pipelines to make sure there are no breaking changes introduced accidentally. The step needs to only be done once for each new version, however if the version is still in development then it can be updated multiple times. -5. Update the index.html file in the electron tool `tools\dicom-web-electron\index.html` to allow for the user to select the new version. - -## Deprecation - -We can deprecate old versions by marking the version as deprecated as follows: -```c# -[ApiVersion("2")] -[ApiVersion("1", Deprecated = true)] -``` - -TBD: When to deprecate and when to retire old versions - -## Communicating changes to customers -TBD: if process is needed for developers to document their changes to communicate to customers diff --git a/docs/development/code-organization.md b/docs/development/code-organization.md deleted file mode 100644 index 8f79794b7e..0000000000 --- a/docs/development/code-organization.md +++ /dev/null @@ -1,49 +0,0 @@ - # Code Organization - -## Projects - The codebase is designed to support different data stores, identity providers, operating systems, and is not tied to any particular cloud or hosting environment. To achieve these goals, the project is broken down into layers: - -| Layer | Example | Comments | -| ------------------ | ------------------------------------------------------------ |---------------------------------------------------------------------------------------| -| Hosting Layer | `Microsoft.Health.Dicom.Web` | Supports hosting in different environments with custom configuration of IoC container. For development purposes only. | -| REST API Layer | `Microsoft.Health.Dicom.Api` | Implements the RESTful DICOMweb™ | -| Core Logic Layer | `Microsoft.Health.Dicom.Core` | Implements core logic to support DICOMweb™ | -| Persistence Layer | `Microsoft.Health.Dicom.Sql` `Microsoft.Health.Dicom.Blob` | Pluggable persistence provider | - -## Patterns - -Dicom server code follows the below **patterns** to organize code in these layers. - -### [MediatR Handler](https://github.com/jbogard/MediatR): - -Used to dispatch messages from the Controller methods. Used to transform requests and responses from the hosting layer to the service. -- Responsibilities: authorization decisions, message deserialization -- Naming Guidelines: `Resource`Handler -- Example: [DeleteHandler](/src/Microsoft.Health.Dicom.Core/Features/Delete/DeleteHandler.cs) - -### [Resource Service](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-6.0): -Used to implement business logic. Like input validation(inline or call), orchestration, or core response objects. -- Responsibilities: implementing Service Class Provider responsibilities, including orchestrating persistence providers. -- Keep the services scoped to the resource operations. -- Naming Guidelines: `Resource`Service -- Example: [IQueryService](/src/Microsoft.Health.Dicom.Core/Features/Query/IQueryService.cs) - -### [Store Service](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-6.0): -Data store specific implementation of storing/retrieving/deleting the data. -- Responsibilities: provide an abstraction to a single persistence store. -- The interface is defined in the core and implementation in the specific persistence layer. -- They should not be accessed outside a service. -- Naming Guidelines: `Resource`Store -- Example: [SqlIndexDataStore](/src/Microsoft.Health.Dicom.SqlServer/Features/Store/SqlIndexDataStore.cs) - -### [Middleware](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-6.0): - Standard/Common concerns like authentication, routing, logging, exception handling that needs to be done for each request, are separated into their own components. - -- Naming Guidelines: `Responsibility`Middleware. -- Example: [ExceptionHandlingMiddleware](/src/Microsoft.Health.Dicom.Api/Features/Exceptions/ExceptionHandlingMiddleware.cs). - -### [Action Filters](https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-6.0): -Dicom code uses pre-action filters. Authorization filters for authentication and Custom filter for acceptable content-type validation. - -- Naming Guidelines: `Responsibility`FilterAttribute. -- Example: [AcceptContentFilterAttribute](/src/Microsoft.Health.Dicom.Api/Features/Filters/AcceptContentFilterAttribute.cs). diff --git a/docs/development/data-validation.md b/docs/development/data-validation.md deleted file mode 100644 index d7446aac41..0000000000 --- a/docs/development/data-validation.md +++ /dev/null @@ -1,15 +0,0 @@ -# Data Validation - -Because DICOM is a historical standard with a long history of implementations that conform to the standard to varying degrees, -we expect DICOM data to vary widely in its strict adherence to the standard. - -**Our general approach is to be has lenient as possible, accepting DICOM data unless it has a direct effect on functionality.** - -This approach plays out in the following ways currently: -1. When DICOM data is received via a STOW request, we only validate data attributes that are indexed by default or via extended query tag. -2. We will attempt to store all other data attributes as they are. -3. When new data attributes are indexed, extended query tag API handles errors gracefully, by continuing on validation errors and -getting explicit consent to allow searching partially indexed data. -4. New functionality should account for the presence of invalid data in unindexed attributes. - -Data validation errors are communicated on each request by response status codes and failure codes documented in the conformance statement. diff --git a/docs/development/exception-handling.md b/docs/development/exception-handling.md deleted file mode 100644 index 9c850f0ab2..0000000000 --- a/docs/development/exception-handling.md +++ /dev/null @@ -1,12 +0,0 @@ -## Exception handling Guidelines - -### The Dicom server code follows below pattern for raising exceptions -- All exceptions thrown in the dicom-server code inherit from base type DicomServerException. -- All user input validation errors throw a derived exception from ValidationException. -- Internal classes use Ensure library to validate input. Ensure library throws .Net Argument*Exception. -- Exceptions from dependent libraries like fo-dicom are caught and wrapped in exception inherited from DicomServerException. -- Exceptions from dependent services libraries like Azure storage blob are caught and wrapped in exception inherited from DicomServerException. - -### The Dicom server code follows below pattern for handling exceptions -- All DicomServerExceptions are handled in middleware [ExceptionHandlingMiddleware](/src/Microsoft.Health.Dicom.Api/Features/Exceptions/ExceptionHandlingMiddleware.cs). These exceptions are mapped to the right status code and response body. -- All unexpected exceptions are logged and mapped to 500 server error. diff --git a/docs/development/functions.md b/docs/development/functions.md deleted file mode 100644 index f0c8368c2e..0000000000 --- a/docs/development/functions.md +++ /dev/null @@ -1,66 +0,0 @@ -# Functions - -We utilize "functions" to offload work to the background. This is useful when work does not need to be transactional -and completed right away. Instances of this are updating or deleting files. - -To do the work, we utilize [Durable Functions](https://learn.microsoft.com/en-us/azure/azure-functions/durable/) which is an extension of Azure Functions that lets you write stateful functions in a serverless compute environment. - -## Breaking Changes -There are several examples of breaking changes to be aware of and examples with how to handle these can be found -[here](https://learn.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-versioning?tabs=csharp#how-to-handle-breaking-changes). - -The specific strategy using in this repo to version functions is by updating the version name within the function -name, known as [side-by-side deployment](https://learn.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-versioning?tabs=csharp#side-by-side-deployments). - -Upon deploying new code with new function versions, it is possible that the function state retained for an -ongoing/running function will be that of a previous version. Because of this, we need to be aware of backwards -compatibility and breaking changes. - -Always keep in mind that the state of a function for orchestration is kept in JSON and de/serialization has to be -able to occur back and forth. Often, to avoid unnecessary complexity, it is easier to version the functions and -even orchestration than to attempt to write tests to ensure everything would continue to work with changes made. - -Also, be sure to clean up the "old" functions quickly after the new ones are released so we do not have stale code. - -## Common Gotchas - -When versioning a function because input arguments have changed, pay attention to the CTOR of the arg and ensure the -field names are equivalent to the properties they are being assigned to. Without doing this, the JSON de/serializer -for the functions host will not understand how to de/serialize the parameters to the CTOR and you may see *null -assigned to a parameter that was otherwise seemingly passed in and non null*. - -## host.json -You'll see that we have some duplication between host.json and the webserver's appsettings. When testing locally, be -sure to set all of the values you'll need so the local host knows how to utilize these features. - -## Testing Locally - -Whether using E2E tests or running the web server, by default the Azure emulator running locally will be used to -store state. If a function fails, it may be necessary to go in to manually cleanup this state by deleting the -DicomTaskHubInstances and DicomTaskHubHistory tables in the Azure Storage Explorer. - -Note that enabling the External Store for IDP just means the dcm file blob will be written externally. All function -fata will continue to be written to the default local store. -If enabling External Store, be sure to also run the test with the flag set: -dotnet test "Microsoft.Health.Dicom.Web.Tests.E2E.dll" --filter "Category=bvt-dp" -e DicomServer__Features__EnableExternalStore="true" - -Often, this flag can also be set in your IDE. For example: -![img.png](img.png) - -### E2E tests - -Run E2E tests and place debug points throughout the function code to step through the code. - -### Making a request to the server to activate a function - -To test locally, you'll want to have two separate IDE windows open: -1. one to run the web server -2. one to run the function host - -This way, you can make a request via Postman or curl to the web server and the function host will pick up the request. -This will allow you to debug and step through the function code. This is the closest we have to testing a full -integration locally aside from using E2E tests. - -## Build Failures -If there's build failures due to a function failing, you can check out the Application Insights app for logs to -discern the cause. Do not ignore these. These should never be flaky and if any are, fix them. diff --git a/docs/development/identity-server-authentication.md b/docs/development/identity-server-authentication.md deleted file mode 100644 index 23a4e49075..0000000000 --- a/docs/development/identity-server-authentication.md +++ /dev/null @@ -1,46 +0,0 @@ -# Using Identity Server for Development - -This article also explains how to manage authentication in development and test scenarios without AAD integration using an Identity Provider. To learn more about the authentication settings, see [Authentication Settings Overview](../how-to-guides/enable-authentication-with-tokens.md#L15). - -For the F5 experience and test environments, an in-process identity provider is included that can act as the authentication provider for the DICOMweb™ API. - -## TestAuthEnvironment.json - -The [`testauthenvironment.json`](/testauthenvironment.json) file located in the root directory holds the configuration used for the server. **This file is meant only for local and test environments.** The items represented in this file include the roles available for the API as well as users and client applications that have access to the API. During the F5 experience and local testing, the password/secret for both users and client applications is the same as the id of the item. - -## Enabling Development Identity Provider for testing - -[Launch settings](/src/Microsoft.Health.Dicom.Web/Properties/launchSettings.json) has `DicomWebSecurityEnabled` profile that has pre-set settings used to enable development identity provider. - -## Authenticating using built in IdentityServer - -To obtain a token issue the following command. - -``` -POST /connect/token HTTP/1.1 -Host: https://localhost:63838 -Content-Type: application/x-www-form-urlencoded - -client_id=globalAdminServicePrincipal&client_secret=globalAdminServicePrincipal&grant_type=client_credentials&scope=health-api -``` - -To authenticate with the Dicom API take the `access_token` from the previous command and attach it as an `Authorization` header with the syntax: `Bearer {access_token}`. - -Example token response - -```json -{ - "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ijc4YWJlMDM0OGEyNDg4NzU0MmUwOGJjNTg3YWFjY2Q4IiwidHlwIjoiSldUIn0.eyJuYmYiOjE1MjM1NTQ3OTQsImV4cCI6MTUyMzU1ODM5NCwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo1MzcyNyIsImF1ZCI6WyJodHRwOi8vbG9jYWxob3N0OjUzNzI3L3Jlc291cmNlcyIsImZoaXItYXBpIl0sImNsaWVudF9pZCI6Imtub3duLWNsaWVudC1pZCIsInNjb3BlIjpbImZoaXItYXBpIl19.pZWIWy3RdDHp5zgcYs8bb9VrxIHXbYu8LolC3YTy6xWsPxMoPUQwbAltYmC6WDXFiDygpsC5ofkGlR4BH0Bt1FMvFWqFYhPcOOKvBqLLc055EHZfTcNcmiUUf4y4KRuQFqWZsH_HrfWwykSGVio2OnYcQvytrbjAi_EzHf2vrHJUHX2JFY4A_F6WpJbQiI1hUVEOd7h1jfmAptWlNGwNRbCF2Wd1Hf_Hodym8mEOKQz21VHdvNJ_B-owPMvLjalV5Nrvpv0yC9Ly5YablrkzB583eHwQNSA7A4ZMm49O8MWv8kUwwF5TF0lJJDyyw3ruqmPWCM-058chenU0rtCsPQ", - "expires_in": 3600, - "token_type": "Bearer" -} -``` - -Example Authorization header -``` -Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6Ijc4YWJlMDM0OGEyNDg4NzU0MmUwOGJjNTg3YWFjY2Q4IiwidHlwIjoiSldUIn0.eyJuYmYiOjE1MjM1NTQ3OTQsImV4cCI6MTUyMzU1ODM5NCwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo1MzcyNyIsImF1ZCI6WyJodHRwOi8vbG9jYWxob3N0OjUzNzI3L3Jlc291cmNlcyIsImZoaXItYXBpIl0sImNsaWVudF9pZCI6Imtub3duLWNsaWVudC1pZCIsInNjb3BlIjpbImZoaXItYXBpIl19.pZWIWy3RdDHp5zgcYs8bb9VrxIHXbYu8LolC3YTy6xWsPxMoPUQwbAltYmC6WDXFiDygpsC5ofkGlR4BH0Bt1FMvFWqFYhPcOOKvBqLLc055EHZfTcNcmiUUf4y4KRuQFqWZsH_HrfWwykSGVio2OnYcQvytrbjAi_EzHf2vrHJUHX2JFY4A_F6WpJbQiI1hUVEOd7h1jfmAptWlNGwNRbCF2Wd1Hf_Hodym8mEOKQz21VHdvNJ_B-owPMvLjalV5Nrvpv0yC9Ly5YablrkzB583eHwQNSA7A4ZMm49O8MWv8kUwwF5TF0lJJDyyw3ruqmPWCM-058chenU0rtCsPQ -``` - -## Resources - -To learn how to manage authentication through Azure AD, see [Azure AD Authentication](../how-to-guides/enable-authentication-with-tokens.md). \ No newline at end of file diff --git a/docs/development/img.png b/docs/development/img.png deleted file mode 100644 index b56d9867e3..0000000000 Binary files a/docs/development/img.png and /dev/null differ diff --git a/docs/development/naming-guidelines.md b/docs/development/naming-guidelines.md deleted file mode 100644 index 63defec646..0000000000 --- a/docs/development/naming-guidelines.md +++ /dev/null @@ -1,4 +0,0 @@ -# Naming Guidelines - -- DICOM code uses the [.Net naming guideline](https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/naming-guidelines) -- DICOM tags defined in the code also use the same guidelines for abbreviations. Ex: StudyInstanceUid. diff --git a/docs/development/roles.md b/docs/development/roles.md deleted file mode 100644 index 5d63fd4770..0000000000 --- a/docs/development/roles.md +++ /dev/null @@ -1,36 +0,0 @@ -# Roles in Microsoft Imaging Server for DICOM - -The medical imaging server uses a role-based access control system. The access control model is based on the following concepts: - -- **Data Actions** refer to specific allowed or disallowed operations that can be performed on a imaging server's data. Examples include `read`, `write`, and `delete`. -- **Role definitions** or simply **roles**, are named collections of actions that are allowed be performed. They apply to a set of **scopes**. -- **Scopes** define the subset of data to which a role definition applies. Currently, only the root scope (`/`) is supported, which means that role definitions apply to all the data in the imaging server. -- **Role assignments** grants a role definition to an identity (user, group, or service principal). - -The set of data actions that can be part of a role definition are: - -- `*` allows all data actions -- `read` is required for reading and searching resources. -- `write`is required for creating or updating resources. -- `delete` is required for deleting resources. - -Roles are defined in the [roles.json](../../src/Microsoft.Health.Dicom.Web/roles.json) file. Administrators can customize them if desired. A role definition looks like this: - -``` json -{ - "name": "globalWriter", - "dataActions": [ - "*" - ], - "notDataActions": [ - "delete" - ], - "scopes": [ - "/" - ] -} -``` - -This role allows all data actions except `delete`. Note that if a user is part of this role and another role that allows `delete`, they will be allowed to perform the action. - -Role assignments are done in the identity provider. In Azure Active Directory, you define app roles on the imaging server's app registration. The app role names must correspond to the names of the roles defined in `roles.json`. Then you assign identities (users, groups, or service principals) to the app roles. \ No newline at end of file diff --git a/docs/development/setup.md b/docs/development/setup.md deleted file mode 100644 index 3ff8b151e0..0000000000 --- a/docs/development/setup.md +++ /dev/null @@ -1,63 +0,0 @@ -# Developing -## Requirements -- [Azurite storage emulator](https://go.microsoft.com/fwlink/?linkid=717179) -- SQL Server 2019 with full text index feature -- .NET core SDK version specified [here](/global.json) - - https://dotnet.microsoft.com/download/dotnet-core/6.0 - -## Getting Started in Visual Studio -### Developing -- Install Visual Studio 2022 -- [Clone the Medical Imaging Server for DICOM repo](https://github.com/microsoft/dicom-server.git) -- Navigate to the cloned repository -- Open `Microsoft.Health.Dicom.sln` in VS -- Build -- Make sure the storage emulator is running -- Run all tests from the Test Explorer - -# Testing -- Set Microsoft.Health.Dicom.Web as your startup project -- Run the project -- Web server is now running at https://localhost:63838/ - -## Posting DICOM files using Fiddler -- [Install fiddler](https://www.telerik.com/download/fiddler) -- Go to Tools->Options->HTTPS on fiddler. Click protocols and add "tls1.2" to the list of protocols. - -![Fiddler Config Image](/docs/images/FiddlerConfig.png) -- Download DCM example file from [here](/docs/dcms) -- Upload DCM file - - Use `Upload file...` link at request body section as shown in picture below - - Located in `Parsed` tab inside the `Composer` tab -- Update request header: - - `Accept: application/dicom+json` - - `Content-Type: multipart/related` **(don't change boundary part)** -- Update request body: - - `Content-Type: application/dicom` -- Post the request to `https://localhost:63838/v/studies` - - Hit Execute button - -![Post A Dicom Image](/docs/images/FiddlerPost.png) -- If the POST is successful, you should be see an HTTP 200 response. - -![Post Succeeds](/docs/images/FiddlerSucceedPost.png) -- Note: you cannot upload the same DCM file again unless it is first deleted. Doing so will result in an HTTP 409 Conflict error. - -## Posting DICOM files using Postman -Postman only supports uploading DICOM files using the single part payload defined in the DICOM standard. This is because Postman cannot support custom separators in a multipart/related POST request. -- [Install Postman](https://www.postman.com/downloads/) -- Set the request to be a post request to the url `https://localhost:63838/v/studies` -- In the `Body` tab of Postman select `binary` and and then select the single part DICOM file you would like to upload -- In the headers section add the following. All the headers should match as in the image below - - Accept: `application/dicom+json` - - Content-Type: `application/dicom` -- Click `Send`, a successful post will result in a 200 OK - -![Postman headers](/docs/images/postman-singlepart-headers.PNG) - -## Postman for Get -- Example QIDO to get all studies -```http -GET https://localhost:63838/v/studies -accept: application/dicom+json -``` diff --git a/docs/development/tests.md b/docs/development/tests.md deleted file mode 100644 index 8bce278eca..0000000000 --- a/docs/development/tests.md +++ /dev/null @@ -1,23 +0,0 @@ -# Tests - -![Testing pyramid](/docs/images/TestPyramid.png) - -- More unit tests, less integration tests, even less EnE tests. -- Unit tests are used to test all combination of valid and invalid cases for a specific component. -- E2E tests to validate all the integrations works for p0 scenario. -- Manual directed bug bashes are used to augment the automated testing. - -## Unit tests -- Smallest testable part of software. -- All business logic pinned with unit tests. - -## Integration tests -- Individual units are combined and tested as a group. -- DICOM uses integration tests to test the persistence layer. - -## End to End (E2E) tests -- End to end user scenarios are tested. -- DICOM uses E2E test methodology to test Web API endpoint behaviors. - -## Function Tests -- See [Functions](functions.md) for more information. diff --git a/docs/how-to-guides/blob-migration.md b/docs/how-to-guides/blob-migration.md deleted file mode 100644 index 7eb979344f..0000000000 --- a/docs/how-to-guides/blob-migration.md +++ /dev/null @@ -1,75 +0,0 @@ -# Blob Migration - -> This documentation is only relevant to the Medical Imaging Server for DICOM open-source project. All data in Azure Health Data Services Dicom Services has already been migrated. - -Currently DICOM files are stored with DICOM UIDs as blob names in blob storage, using the template `{account}/{container}/{studyUid}/{seriesUid}/{sopInstanceUid}_{watermark}.dcm`. -You can see this naming scheme in your blob container if you've saved any files. Here's an example using the blue circle sample image: - -![dicomwebcontainer-bluecircle-old-blob-format](../images/dicomwebcontainer-bluecircle-old-blob-format.png) - -Since UIDs may include personal information about the context of their creation, such as patient information or identifiers, we made the decision to change the way that we store DICOM files. In the next sections we list the steps to migrate your existing blobs from the old format to the new format. - -## Blob Migration Configuration -Below is the `appsettings.json` configuration related to blob migration. Several properties need to be updated to trigger migration. - -```json -"DicomServer": { - "Services": { - "BlobMigration": { - "FormatType": "Old", - "StartCopy": false, - "StartDelete": false, - "CopyFileOperationId": "1d4689da-ca3b-4659-b0c7-7bf6c9ff25e1", - "DeleteFileOperationId": "ce38a27e-b194-4645-b47a-fe91c38c330f", - "CleanupDeletedFileOperationId": "d32a0469-9c27-4df3-a1e8-12f7f8fecbc8", - "CleanupFilterTimeStamp": "2022-08-01" - } - } -} -``` - -These settings can be adjusted as part of the Azure App Service's configuration settings if you used the [deploy to Azure option](https://github.com/microsoft/dicom-server#deploy-to-azure) from our README: - -![app-service-settings-configuration](../images/app-service-settings-configuration.png) - - -## Migration Steps - -### If deployed new service and have not created any files yet -1. [You can upgrade to the latest version of the service](../resources/dicom-server-maintaince-guide.md) and skip the migration steps. The configuration section `Blob Migration` will not be present when service is on the latest version. - -### If you have already uploaded DICOM files but do not care about migrating the data -1. If you have already uploaded DICOM files but do not care about migrating the data, you can use the [Delete API](../resources/conformance-statement.md#delete) to delete all existing studies. -2. [You can upgrade to the latest version of the service](../resources/dicom-server-maintaince-guide.md) and skip the migration steps. The configuration section `Blob Migration` will not be present when service is on the latest version. - -### If you have already uploaded DICOM files and want to migrate the data -If you have already uploaded DICOM files and want to migrate the data, you will need to execute the following steps before upgrading. This scenario has two options depending on whether if you want interruption to the service or not. Make sure Azure Monitor is configured to monitor the service before starting the migration (for more info on how to configure Azure monitor, please refer to [the Azure Monitor guide](../how-to-guides/configure-dicom-server-settings.md#azure-monitor)). - -#### With Service Interruption -If you are ok with interruption to the service, you can follow the steps below. The interruption here is a self-managed one and using the service while the copying is occuring and before switching to the new format can corrupt the data path or retrieve wrong data. - -1. Set `BlobMigration.StartCopy` to `true` and restart the service. - 1. When restarting the service, ensure it is in such a way that it picks up the new application settings and this will vary by how your service is deployed. - 2. This will trigger the `CopyFiles` Durable Function which will copy the old format DICOM files to the new format. - 3**Do not use the service at this time**. We want to make sure all files are copied over. -2. To ensure the Copy operation has been completed, you can check Azure Monitor logs for a `"Completed copying files."` message. This will indicate that the operation has been completed: - - ![dicomwebcontainer-bluecircle-copy-logs](../images/dicomwebcontainer-bluecircle-copy-logs.png) - - At this time, you'll have both the new and old files: - - ![dicomwebcontainer-bluecircle-old-blob-format-dual](../images/dicomwebcontainer-bluecircle-old-blob-format-dual.png) - -3. Once the copy is completed, you can change `BlobMigration.FormatType` to `"New"` and `BlobMigration.StartDelete` to `true` and restart the service. - 1. This will trigger a Durable Function which will delete all the old format blobs only if corresponding new format blobs exist. This is a safe operation and doesn't delete any blobs without checking for the existence of new format blobs. - 2. **Start using your service** to store and retrieve data, which will work with the `New` format. -4. To ensure Delete has been completed, you can check Azure Monitor logs for `"Completed deleting files."` message. This will indicate that the delete has been completed. - -#### Without Service Interruption -If you are not ok with interruption to the service, you can follow the steps below. - -1. Change `BlobMigration.FormatType` to `"Dual"`. This will duplicate any new DICOM files uploaded to both old and new format as you continue to use your service during the copying operation. -2. Follow steps in [With Service Interruption](#with-service-interruption), but feel free to continue using the service. - -> **Please post any questions or issues encountered during migration in the related [GitHub Discussion](https://github.com/microsoft/dicom-server/discussions/1561).** - diff --git a/docs/how-to-guides/configure-dicom-server-settings.md b/docs/how-to-guides/configure-dicom-server-settings.md deleted file mode 100644 index 7a7532da84..0000000000 --- a/docs/how-to-guides/configure-dicom-server-settings.md +++ /dev/null @@ -1,116 +0,0 @@ -# Configure Medical Imaging Server for DICOM Settings - -This How-to Guide explains how to configure settings for the Medical Imaging Server for DICOM after deployment. - -## Prerequisites - -To configure your Medical Imaging Server for DICOM, you need to have an instance deployed. If you have not already deployed the Medical Imaging Server, [deploy an instance to Azure](../quickstarts/deploy-via-azure.md). - -## Manage Authentication - -To configure authentication for the Medical Imaging Server for DICOM using Azure AD, see [Enable Authentication with Tokens](../how-to-guides/enable-authentication-with-tokens.md). - -To manage authentication in development and test scenarios without AAD integration using an Identity Provider, see [Identity Server Authentication](../development/identity-server-authentication.md). - -## Manage Azure App Service - -The S1 tier is the default App Service Plan SKU enabled upon deployment. Azure offers a variety of plans to meet your workload requirements. To learn more about the various plans, view the [App Service pricing](https://azure.microsoft.com/pricing/details/app-service/windows/). - -If you would like to Scale Up your App Service plan to a different tier: - -1. Navigate to your Medical Imaging Server for DICOM **App Service** in the Azure Portal. -1. Select **Scale up (App Service plan)** from the menu: -![Scale Up](../images/scale-up-1.png) -1. Select the plan that fits your workload requirements: -![Scale Up 2](../images/scale-up-2.png) -1. Click **Apply**. - -Autoscale is a built-in feature that helps applications perform their best when demand changes. You can choose to scale your resource manually to a specific instance count, or via a custom Autoscale policy that scales based on metric(s) thresholds, or scheduled instance count which scales during designated time windows. Autoscale enables your resource to be performant and cost effective by adding and removing instances based on demand - -In addition to Scale Up, you can also Scale Out your App Service Plan to meet the requirements of your workload. You can select to manually scale your service to maintain a fixed instance count, or custom autoscale your service based on any metrics. If you would like to Scale Out your App Service Plan: - -1. Navigate to your Medical Imaging Server for DICOM **App Service** in the Azure Portal. -1. Select **Scale out (App Service plan)** from the menu: -![Scale Out](../images/scale-out-1.png) -1. Choose the Scale Out option that best fits your requirements: -![Scale Out 2](../images/scale-out-2.png) -1. Select **Save**. - -For suggested guidance on Azure App Service tiers, see [Medical Imaging Server for DICOM Performance Guidance](../resources/performance-guidance.md). - -## Manage SQL Database - -The Standard tier of the DTU-based SQL performance tiers is enabled by default upon deployment. In DTU-based SQL purchase models, a fixed set of resources is assigned to the database via performance tiers: Basic, Standard and Premium. To learn more about the various tiers, view the [Azure SQL Database Pricing](https://azure.microsoft.com/pricing/details/sql-database/single/). - -If you would like to update your SQL Database tier: - -1. Navigate to the **SQL Database** you created when you deployed the Medical Imaging Server for DICOM. -1. Select **Configure**: -![Configure Sql1](../images/configure-sql-1.png) -1. Choose the performance tier and DTU level that meets your workload requirements: -![Configure SQL2](../images/configure-sql-2.png) -1. Click **Apply**. - - -For suggested guidance on SQL Database Tiers, see [Medical Imaging Server for DICOM Performance Guidance](../resources/performance-guidance.md). - -## Additional Configuration Settings - -## Azure Monitor - - -[Azure Monitor](https://docs.microsoft.com/azure/azure-monitor/overview) offers a variety of solutions to collect, analyze and act on telemetry, including Application Insights Log Analytics. - -### Application Insights - -If you deploy the Medical Imaging Server for DICOM with our [Quickstart Deploy to Azure](../quickstarts/deploy-via-azure.md), Application Insights is deployed and enabled by default. To view and customize Application Insights: - -1. Navigate to your Medical Imaging Server for DICOM **Application Insights** resource. -1. Select **Availability**, **Failures** or **Performance** for insight into the performance of your App Service. -1. To link your Application Insights resource to your Medical Imaging Server for DICOM Web App: - 1. Navigate to your Medical Imaging Server for DICOM **App Service**. - 1. Select **Application Insights** under **Settings**. Select *Enable* Application Insights. Select the existing Application Insights resource which was deployed. - 1. Optionally, you can enable Application Insights features like *Profiler*, *Snapshot Debugger* and *SQL Commands*. (Note, these can be turned on later). - 1. Click *Apply*. -1. To learn how to customize Application Insights for your requirements, see [Application Insights Overview](https://docs.microsoft.com/azure/azure-monitor/app/app-insights-overview). - -If you did not enable Application Insights during deployment, you can via the Azure Portal: - -1. Navigate to your Medical Imaging Server for DICOM **App Service**. -1. Select **Application Insights** from the menu: -![App Insights 1](../images/app-insights-1.png) -1. Select **Turn on Application Insights**: -![App Insights 2](../images/app-insights-2.png) -1. Link your App Service to an Application Insights Resource. You can create a new name for your **Application Insights** resource or use the default name. Select **Apply**. -1. View and customize **Application Insights** by navigating to the created **Application Insights** resource. - -### Diagnostic Settings & Log Analytics - -To monitor your SQL Database, create diagnostic settings which stream to Log Analytics: - -1. Navigate to your **SQL Database**. -1. Select **Diagnostic Settings**: -![Diagnostic settings 1](../images/diagnostic-settings-1.png) -1. Select **Add Diagnostic setting**: -![Diagnostic settings 2](../images/diagnostic-settings-2.png) -1. Select the log and/or metric diagnostic settings you would like to monitor, along with the destination for those logs and/or metrics: -![Diagnostic settings 3](../images/diagnostic-settings-3.png) -1. Select **Save**. - -To learn how to customize your diagnostic settings further, see [Diagnostic Settings](https://docs.microsoft.com/azure/azure-monitor/platform/diagnostic-settings?WT.mc_id=Portal-Microsoft_Azure_Monitoring). To learn how to write queries with Log Analytics, see [Log Query Overview](https://docs.microsoft.com/azure/azure-monitor/log-query/log-query-overview). - -## OHIF Viewer - -By default, OHIF Viewer is enabled when you deploy the Medical Imaging Server for DICOM to Azure. To update this setting: - -1. Navigate to your Medical Imaging Server for DICOM **App Service** in the Azure Portal. -1. Select **Configuration** from the menu: -![OHIF Viewer1](../images/ohif-viewer-1.png) -1. Select the *Edit* button for **DicomServer:Features:EnableOhifViewer**: -![OHIF Viewer1](../images/ohif-viewer-2.png) -1. Update the **Value** to *False* and select **Ok**. -1. Click **Save** to update the setting. - -## Summary - -This How-to Guide explained how to configure settings for the Medical Imaging Server for DICOM after deployment. Once your Medical Imaging Server for DICOM is deployed and configured, you can [Use Medical Imaging Server for DICOM APIs](../tutorials/use-the-medical-imaging-server-apis.md). diff --git a/docs/how-to-guides/enable-authentication-with-tokens.md b/docs/how-to-guides/enable-authentication-with-tokens.md deleted file mode 100644 index 93d602a001..0000000000 --- a/docs/how-to-guides/enable-authentication-with-tokens.md +++ /dev/null @@ -1,105 +0,0 @@ -# Azure Active Directory Authentication - -This How-to Guide shows you how to configure the authentication settings for the Medical Imaging Server for DICOM through Azure. To complete this configuration, you will: - -1. **Create a resource application in Azure AD**: This resource application will be a representation of the Medical Imaging Server for DICOM that can be used to authenticate and obtain tokens. In order for an application to interact with Azure AD, it needs to be registered. -1. **Provide app registration details to your Medical Imaging Server for DICOM**: Once the resource application is registered, you will set the authentication of your Medical Imaging Server for DICOM App Service. -1. **Create a service client application in Azure AD**: Client application registrations are Azure AD representations of applications that can be used to authenticate and obtain tokens. A service client is intended to be used by an application to obtain an access token without interactive authentication of a user. It will have certain application permissions and use an application secret (password) when obtaining access tokens. -1. **Retrieve Access Token via Postman or Azure CLI**: With your service client application enabled, you can obtain an access token to authenticate your application. - -## Prerequisites - -1. Deploy a [Medical Imaging Server for DICOM to Azure](../quickstarts/deploy-via-azure.md). -1. This tutorial requires an Azure AD tenant. If you have not created a tenant, see [Create a new tenant in Azure Active Directory](https://docs.microsoft.com/azure/active-directory/fundamentals/active-directory-access-create-new-tenant). - -## Authentication Settings Overview - -The current authentication settings exposed in configuration are the following: - -```json - -{ - "DicomServer" : { - "Security": { - "Enabled": true, - "Authentication": { - "Audience": "", - "Authority": "" - } - } - } -} -``` - -| Element | Description | -| -------------------------- | --- | -| Enabled | Whether or not the server has any security enabled. | -| Authentication:Audience | Identifies the recipient that the token is intended for. This is set automatically by the `DevelopmentIdentityProvider`. | -| Authentication:Authority | The issuer of the jwt token. This is set automatically by the `DevelopmentIdentityProvider`. | - -## Authentication with Azure AD - -### Create a Resource Application in Azure AD for your Medical Imaging Server for DICOM - -1. Sign into the [Azure Portal](https://ms.portal.azure.com/). Search for **App Services** and select your Medical Imaging Server for DICOM App Service. Copy the **URL** of your Dicom App Service. -1. Select **Azure Active Directory** > **App Registrations** > **New registration**: - 1. Enter a **Name** for your app registration. - 2. In **Redirect URI**, select **Web** and enter the **URL** of your Medical Imaging Server for DICOM App Service. - 3. Select **Register**. -1. Select **Expose an API** > **Set**. You can specify a URI as the **URL** of your app service or use the generated App ID URI. Select **Save**. -1. Select **Add a Scope**: - 1. In **Scope name**, enter *user_impersonation*. - 1. In the text boxes, add an admin consent display name and admin consent description you want users to see on the consent page. For example, *access my app*. - -### Set the Authentication of your App Service - -1. Navigate to your Medical Imaging Server for DICOM App Service that you deployed to Azure. -1. Select **Configuration** to update the **Audience**, **Authority**, and **Security:Enabled**: - 1. Set the **Application ID URI** enabled above as the **Audience**. - 1. **Authority** is whichever tenant your application exists in, for example: ```https://login.microsoftonline.com/.onmicrosoft.com```. - 1. Set **Security:Enabled** to be ```True```. - 1. Save your changes to the configuration. - -### Create a Service Client Application - -1. Select **Azure Active Directory** > **App Registrations** > **New registration**: - 1. Enter a **Name** for your service client. You can provide a **URI** but it typically will not be used. - 1. Select **Register**. -1. Copy the **Application (client) ID** and the **Directory (tenant) ID** for later. -1. Select **API Permissions** to provide your service client permission to your resource application: - 1. Select **Add a permission**. - 1. Under **My APIs**, select the resource application you created above for your Dicom App Service. - 1. Under **Select Permissions**, select the application roles from the ones that you defined on the resource application. - 1. Select **Add permissions**. -1. Select **Certificates & secrets** to generate a secret for obtaining tokens: - 1. Select **New client secret**. - 1. Provide a **Description** and duration of the secret. Select **Add**. - 1. Copy the secret once it has been created. It will only be displayed once in the portal. - -### Get Access Token Using Azure CLI - -1. First, update the application you create above to have access to the Azure CLI: - 1. Select **Expose an API** > **Add a Client Application**. - 1. For **Client ID**, provide the client ID of Azure CLI: **04b07795-8ddb-461a-bbee-02f9e1bf7b46**. *Note this is available at the [Azure CLI Github Repository](https://github.com/Azure/azure-cli/blob/24e0b9ef8716e16b9e38c9bb123a734a6cf550eb/src/azure-cli-core/azure/cli/core/_profile.py#L65)*. - 1. Select your **Application ID URI** under **Authorized Scopes**. - 1. Select **Add Application**. -1. [Install Azure CLI](https://docs.microsoft.com/cli/azure/install-azure-cli?view=azure-cli-latest). -1. Login to Azure: ```az account``` -1. Request access token using the **Application ID URI** set above: ```az account get-access-token --resource=``` - -### Get Access Token Using Postman - -1. [Install Postman](https://www.postman.com/downloads/) or use the [Postman Web App](https://web.postman.co/). -1. Create a new **Post** Request with the following form-data: - 1. URL: ```//oauth2/token``` where **Authority** is the tenant your application exists in, configured above, and **Tenant ID** is from your Azure App Registration. - 1. If using Azure Active Directory V2 then instead use URL: ```//oauth2/v2.0/token```. - 1. *client_id*: the **Client ID** for your Service Client. - 1. *grant_type*: "client_credentials" - 1. *client_secret*: the **Client secret** for your Service Client. - 1. *resource*: the **Application ID URI** for your Resource Application. - 1. If using Azure Active Directory V2 then instead of setting *resource*, set *scope*: ```/.default``` where Application ID URI is for your Resource Application. -1. Select **Send** to retrieve the access token. - -## Summary - -In this How-to Guide, you learned how to configure the authentication settings for the Medical Imaging Server for DICOM through Azure. To learn how to manage authentication in development and test scenarios, see [Using Identity Server for Development](../development/identity-server-authentication.md). diff --git a/docs/how-to-guides/enable-authorization.md b/docs/how-to-guides/enable-authorization.md deleted file mode 100644 index dfac3ba1d0..0000000000 --- a/docs/how-to-guides/enable-authorization.md +++ /dev/null @@ -1,60 +0,0 @@ -# Azure Active Directory Authorization - -This How-to Guide shows you how to configure the authorization settings for the Medical Imaging Server for DICOM through Azure. To complete this configuration, you will: - -1. **Update a resource application in Azure AD**: This resource application will be a representation of the Medical Imaging Server for DICOM that can be used to authorization and obtain tokens. The application registration will need to be updated to create appRoles. -1. **Assign the application roles in Azure AD**: Client application registrations, users, and groups need to be assigned the roles defined on the application registration. -1. **Provide configuration to your Medical Imaging Server for DICOM**: Once the resource application is updated, you will set the authorization settings of your Medical Imaging Server for DICOM App Service. - -## Prerequisites - -1. **Complete the authentication configuration**: Instructions for enabling authentication can be found in the [Azure Active Directory Authentication](enable-authentication-with-tokens.md) article. - -## Authorization Settings Overview - -The current authorization settings exposed in configuration are the following: - -```json - -{ - "DicomServer" : { - "Security": { - "Authorization": { - "Enabled": true, - "RolesClaim": "role", - "Roles": [ - - ] - } - } - } -} -``` - -| Element | Description | -| -------------------------- | --- | -| Authorization:Enabled | Whether or not the server has any authorization enabled. | -| Authorization:RolesClaim | Identifies the jwt claim that contains the assigned roles. This is set automatically by the `DevelopmentIdentityProvider`. | -| Authorization:Roles | The defined roles. The roles are defined via the `roles.json`. [Additional information can be found here](../development/roles.md) | - -## Authorization setup with Azure AD - -### Azure AD Instructions - -#### Creating App Roles -The instructions for adding app roles to an AAD application can be found [in this documentation article](https://docs.microsoft.com/azure/active-directory/develop/howto-add-app-roles-in-azure-ad-apps). This documentation also optionally shows you how to assign an app role to an application. - -The app roles created need to match the name of the roles found in the `roles.json`. - -#### Assigning Users to App Role -This can be accomplished [via the Azure Portal](https://docs.microsoft.com/en-us/azure/active-directory/manage-apps/add-application-portal-assign-users) or [via a PowerShell cmdlet](https://docs.microsoft.com/en-us/azure/active-directory/manage-apps/assign-user-or-group-access-portal#assign-users-and-groups-to-an-app-using-powershell). - -### Provide configuration to your Medical Imaging Server for DICOM -1. Make sure that you have deployed the `roles.json` to your web application -1. Update the configuration to have the following two settings - * `DicomServer:Security:Authorization:Enabled` = `true` - * `DicomServer:Security:Authorization:RolesClaim` = `"role"` - -## Summary - -In this How-to Guide, you learned how to configure the authorization settings for the Medical Imaging Server for DICOM through Azure. \ No newline at end of file diff --git a/docs/how-to-guides/export-data.md b/docs/how-to-guides/export-data.md deleted file mode 100644 index 9e24c2841f..0000000000 --- a/docs/how-to-guides/export-data.md +++ /dev/null @@ -1,132 +0,0 @@ -# Export DICOM Files - -The Medical Imaging Server for DICOM supports the bulk export of data to an [Azure Blob Storage account](https://azure.microsoft.com/en-us/services/storage/blobs/). Before starting, be sure that the feature is enabled in your [appsettings.json](../../src/Microsoft.Health.Dicom.Web/appsettings.json) or environment by setting the value `DicomServer:Features:EnableExtendedExport` to `true`. - -The export API is available at `POST /export`. Given a *source*, the set of data to be exported, and a *destination*, the location to which data will be exported, the endpoint returns a reference to the newly-started long-running export operation. The duration of this operation depends on the volume of data to be exported. - -## Example - -The below example requests the export of the following DICOM resources to the blob container named `"export"` in the local Azure storage emulator: -- All instances within the study whose Study Instance UID is `1.2.3` -- All instances within the series whose Study Instance UID is `12.3` and Series Instance UID is `4.5.678` -- The instance whose Study Instance UID is `123.456`, Series Instance UID is `7.8`, and SOP Instance UID is `9.1011.12` - -```http -POST /export HTTP/1.1 -Accept: */* -Content-Type: application/json - -{ - "sources": { - "type": "identifiers", - "settings": { - "values": [ - "1.2.3", - "12.3/4.5.678", - "123.456/7.8/9.1011.12" - ] - } - }, - "destination": { - "type": "azureblob", - "settings": { - "blobContainerName": "export", - "connectionString": "UseDevelopmentStorage=true" - } - } -} -``` - -## Request - -The request body consists of the export source and destination. - -```json -{ - "source": { - "type": "identifiers", - "settings": { - "setting1": "", - "setting2": "" - } - }, - "destination": { - "type": "azureblob", - "settings": { - "setting3": "" - } - } -} -``` - -### Source Settings - -The only setting is the list of identifiers to export. - -| Property | Required | Default | Description | -| :------- | :------- | :------ | :---------- | -| `Values` | Yes | | A list of one or more DICOM studies, series, and/or SOP instances identifiers in the format of `"[/[/]]"` | - -### Destination Settings - -The connection to the Azure Blob storage account can be specified with either a `ConnectionString` and `BlobContainerName` or a `BlobContainerUri`. One of these settings is required. - -If the storage account requires authentication, a [SAS token](https://docs.microsoft.com/en-us/azure/storage/common/storage-sas-overview) can be included in either the `ConnectionString` or `BlobContaienrUri` with `Write` permissions for `Objects` in the `Blob` service. A managed identity can also be used to access the storage account with the `UseManagedIdentity` option. The identity must be used by both the DICOM server and the functions, and furthermore it must be assigned the [`Storage Blob Data Contributor`](https://docs.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#storage-blob-data-contributor) role. - -| Property | Required | Default | Description | -| :------------------- | :------- | :------ | :---------- | -| `BlobContainerName` | No | `""` | The name of a blob container. Only required when `ConnectionString` is specified | -| `BlobContainerUri` | No | `""` | The complete URI for the blob container | -| `ConnectionString` | No | `""` | The [Azure Storage connection string](https://docs.microsoft.com/en-us/azure/storage/common/storage-configure-connection-string) that must minimally include information for blob storage | -| `UseManagedIdentity` | No | `false` | An optional flag indicating whether managed identity should be used to authenticate to the blob container | - -## Response - -Upon successfully starting an export operation, the export API returns a `202` status code. The body of the response contains a reference to the operation, while the value of the `Location` header is the URL for the export operation's status (the same as `href` in the body). - -Inside of the destination container, the DCM files can be found with the following path format: `/results///.dcm` - -```http -HTTP/1.1 202 Accepted -Content-Type: application/json - -{ - "id": "df1ff476b83a4a3eaf11b1eac2e5ac56", - "href": "//operations/df1ff476b83a4a3eaf11b1eac2e5ac56" -} -``` - -### Operation Status -The above `href` URL can be polled for the current status of the export operation until completion. A terminal state is signified by a `200` status instead of `202`. - -```http -HTTP/1.1 200 OK -Content-Type: application/json - -{ - "operationId": "df1ff476b83a4a3eaf11b1eac2e5ac56", - "type": "export", - "createdTime": "2022-09-08T16:40:36.2627618Z", - "lastUpdatedTime": "2022-09-08T16:41:01.2776644Z", - "status": "completed", - "results": { - "errorHref": "/4853cda8c05c44e497d2bc071f8e92c4/errors.log", - "exported": 1000, - "skipped": 3 - } -} -``` - -## Errors - -If there are any errors when exporting a DICOM file (that was determined not to be a problem with the client), then the file is skipped and its corresponding error is logged. This error log is also exported alongside the DICOM files and can be reviewed by the caller. The error log can be found at `//errors.log`. - -### Format - -Each line of the error log is a JSON object with the following properties. Note that a given error identifier may appear multiple times in the log as each update to the log is processed *at least once*. - -| Property | Description | -| ------------ | ----------- | -| `Timestamp` | The date and time when the error occurred | -| `Identifier` | The identifier for the DICOM study, series, or SOP instance in the format of `"[/[/]]"` | -| `Error` | The error message | diff --git a/docs/how-to-guides/pull-changes-from-change-feed.md b/docs/how-to-guides/pull-changes-from-change-feed.md deleted file mode 100644 index 72748399be..0000000000 --- a/docs/how-to-guides/pull-changes-from-change-feed.md +++ /dev/null @@ -1,39 +0,0 @@ -# Pull DICOM changes using the Change Feed - -The Change Feed offers customers the ability to go through the history of the Medical Imaging Server for DICOM and act on the create and delete events in the service. This How-to Guide shows you how to consume Change Feed. - -The Change Feed is accessed using REST APIs documented in [Change Feed Concept](/docs/concepts/change-feed.md), it also provides example usage of Change Feed. - -## Consume Change Feed - -Sample C# code using the provided DICOM client package: - -```csharp -public async Task> RetrieveChangeFeedAsync(long offset, CancellationToken cancellationToken) -{ - var _dicomWebClient = new DicomWebClient( - new HttpClient { BaseAddress = dicomWebConfiguration.Endpoint/v }, - sp.GetRequiredService(), - tokenUri: null); - DicomWebResponse> result = await _dicomWebClient.GetChangeFeed( - $"?offset={offset}&limit={DefaultLimit}&includeMetadata={true}", - cancellationToken); - - if (result?.Value != null) - { - return result.Value; - } - - return Array.Empty(); -} -``` - -You can find the full code available here: [Consume Change Feed](../../converter/dicom-cast/src/Microsoft.Health.DicomCast.Core/Features/DicomWeb/Service/ChangeFeedRetrieveService.cs) - -## Summary - -This How-to Guide demonstrates how to consume Change Feed. Change Feed allows you to monitor the history of the Medical Imaging Server for DICOM. To learn more about Change Feed, refer to the [Change Feed Concept](../concepts/change-feed.md). - -### Next Steps - -DICOM Cast polls for any changes via Change Feed, which allows synchronizing the data from a Medical Imaging Server for DICOM to an Azure API for FHIR server. To learn more DICOM Cast, refer to the [DICOM Cast Concept](../concepts/dicom-cast.md). diff --git a/docs/how-to-guides/sync-dicom-metadata-to-fhir.md b/docs/how-to-guides/sync-dicom-metadata-to-fhir.md deleted file mode 100644 index a77874dce0..0000000000 --- a/docs/how-to-guides/sync-dicom-metadata-to-fhir.md +++ /dev/null @@ -1,96 +0,0 @@ -# Sync Medical Imaging Server for DICOM metadata into FHIR Server for Azure - -In this How-to Guide, you will learn how to sync Medical Imaging Server for DICOM metadata with FHIR. To do this, you will learn how to enable DICOM Cast by authentication with Managed Identity. - -For healthcare organizations seeking to integrate clinical and imaging data through the FHIR® standard, DICOM Cast enables synchronizing of DICOM image changes to FHIR™ ImagingStudy resource. This allows you to sync DICOM studies, series and instances into the FHIR™ ImagingStudy resource. - -Once you have competed the [prerequisites](#Prerequisites) and [enabled authentication](#Configure-Authentication-using-Managed-Identity) between your Medical Imaging Server for DICOM, your FHIR Server and your DICOM Cast deployment, you have enabled DICOM Cast. When you upload files to your Medical Imaging Server for DICOM, the corresponding FHIR resources will be persisted in your FHIR server. - -To learn more about DICOM Cast, see the [DICOM Cast Concept](../concepts/dicom-cast.md). - -## Prerequisites - -To enable DICOM Cast, you need to complete the following steps: - -1. [Deploy a Medical Imaging Server for DICOM](../quickstarts/deploy-via-azure.md) -1. [Deploy a FHIR Server](https://github.com/microsoft/fhir-server) -1. [Deploy DICOM Cast](../quickstarts/deploy-dicom-cast.md) - -> NOTE: When deploying a OSS FHIR Server, set the **Sql Schema Automatic Updates Enabled** setting to be *true*. This determines whether the sql schema should be automatically initialized and upgraded on server setup. - -## Configure Authentication using Managed Identity - -Currently there are three types of authentication supported for both the FHIR Server for Azure and the Medical Imaging Server for DICOM: Managed Identity, OAuth2 Client Credential and OAuth2 User Password. The authentication can be configured via the application settings by the appropriate values in the `Authentication` property of the given server. For details on the three types, see [DICOM Cast authentication](/converter/dicom-cast/docs/authentication.md). - -This section will provide an end to end guide for configuring authentication with Managed Identity. - -### Create a resource application for FHIR and DICOM servers - -For both your FHIR and DICOM servers, you will create a resource application in Azure. Follow the instructions below for each server, once for your Medical Imaging Server for DICOM and once for your FHIR Server. - -1. Sign into the [Azure Portal](https://ms.portal.azure.com/). Search for **App Services** and select the FHIR or DICOM App Service. Copy the **URL** of the App Service. -1. Select **Azure Active Directory** > **App Registrations** > **New registration**: - 1. Enter a **Name** for your app registration. - 2. In **Redirect URI**, select **Web** and enter the **URL** of your App Service. - 3. Select **Register**. -1. Select **Expose an API** > **Set**. You can specify a URI as the **URL** of your app service or use the generated App ID URI. Select **Save**. -1. Select **Add a Scope**: - 1. In **Scope name**, enter *user_impersonation*. - 1. In the text boxes, add an admin consent display name and admin consent description you want users to see on the consent page. For example, *access my app*. - -### Set the Authentication for your FHIR & DICOM App Services - -For both your FHIR and DICOM servers, you will set the Audience and Authority for Authentication. Follow the instructions below for each server, once for your Medical Imaging Server for DICOM and once for your FHIR Server. - -1. Navigate to the App Service that you deployed to Azure. -1. Select **Configuration** to update the **Audience**, **Authority**, and **Security:Enabled**: - 1. Set the **Application ID URI** from the App Service as the **Audience**. - 1. **Authority** is whichever tenant your application exists in, for example: ```https://login.microsoftonline.com/.onmicrosoft.com```. - 1. Set **Security:Enabled** to be ```True```. - 1. Save your changes to the configuration. - -### Update Key Vault for DICOM Cast - -1. Navigate to the DICOM Cast Key Vault that was created when you deployed DICOM Cast. -1. Select **Access Policies** in the menu bar and click **Add Access Policy**. - 1. Under **Configure from template**, select **Secret Management**. - 1. Under **Select principal**, click **None selected**. Search for your Service Principle, click **Select** and then **Add**. - 1. Select **Save**. -1. Select **Secrets** in the menu bar and click **Generate/Import**. Use the tables below to add secrets for your DICOM and FHIR servers. For each secret, use the **Manual Upload option** and click **Create**: - -#### Medical Imaging Server for DICOM Secrets - -| Name | Value | -| :------- | :----- | -| DICOM--Endpoint | `````` | -| DicomWeb--Authentication--Enabled | true | -| DicomWeb--Authentication--AuthenticationType | ManagedIdentity | -| DicomWeb--Authentication--ManagedIdentityCredential--Resource | `````` | - -#### FHIR Server Secrets - -| Name | Value | -| :------- | :----- | -| Fhir--Endpoint | `````` | -| Fhir--Authentication--Enabled | true | -| Fhir--Authentication--AuthenticationType | ManagedIdentity | -| Fhir--Authentication--ManagedIdentityCredential--Resource | `````` | - -### Restart Azure Container Instance for DICOM Cast - -Now that you have enabled Authentication for DICOM Cast, you have to Stop and Start the Azure Container Instance to pickup the new configurations: - -1. Navigate to the Container Instance created when you deployed DICOM Cast. -1. Click **Stop** and then **Start**. - -## Summary - -In this How-to Guide, you learned how to enable DICOM Cast by authentication with Managed Identity. Now you can upload DICOM files to your Medical Imaging Server for DICOM, and the corresponding FHIR resources will be populated in your FHIR server. - -To manage authentication with OAuth2 Client Credentials or OAuth2 User Passwords, see [DICOM Cast authentication](/converter/dicom-cast/docs/authentication.md). - -For an overview of DICOM Cast, see [DICOM Cast Concept](../concepts/dicom-cast.md). - -To upload files to your DICOM Server, refer to [Use the Medical Imaging Server APIs](../tutorials/use-the-medical-imaging-server-apis.md). - -You can [Access FHIR Server with Postman](https://docs.microsoft.com/azure/healthcare-apis/access-fhir-postman-tutorial) to see the FHIR resources populated via DICOM Cast. diff --git a/docs/images/DICOM-arch.png b/docs/images/DICOM-arch.png deleted file mode 100644 index dba5c1bd06..0000000000 Binary files a/docs/images/DICOM-arch.png and /dev/null differ diff --git a/docs/images/FiddlerConfig.png b/docs/images/FiddlerConfig.png deleted file mode 100644 index 5dd0ebe583..0000000000 Binary files a/docs/images/FiddlerConfig.png and /dev/null differ diff --git a/docs/images/FiddlerPost.png b/docs/images/FiddlerPost.png deleted file mode 100644 index c4161700eb..0000000000 Binary files a/docs/images/FiddlerPost.png and /dev/null differ diff --git a/docs/images/FiddlerSucceedPost.png b/docs/images/FiddlerSucceedPost.png deleted file mode 100644 index 4a19ac1f0d..0000000000 Binary files a/docs/images/FiddlerSucceedPost.png and /dev/null differ diff --git a/docs/images/TestPyramid.png b/docs/images/TestPyramid.png deleted file mode 100644 index 798466bdf1..0000000000 Binary files a/docs/images/TestPyramid.png and /dev/null differ diff --git a/docs/images/api-headers-example.PNG b/docs/images/api-headers-example.PNG deleted file mode 100644 index 63a30ecf3f..0000000000 Binary files a/docs/images/api-headers-example.PNG and /dev/null differ diff --git a/docs/images/app-insights-1.png b/docs/images/app-insights-1.png deleted file mode 100644 index e77569af3d..0000000000 Binary files a/docs/images/app-insights-1.png and /dev/null differ diff --git a/docs/images/app-insights-2.png b/docs/images/app-insights-2.png deleted file mode 100644 index c91d9f27f3..0000000000 Binary files a/docs/images/app-insights-2.png and /dev/null differ diff --git a/docs/images/app-insights-3.png b/docs/images/app-insights-3.png deleted file mode 100644 index 9b200c9b9c..0000000000 Binary files a/docs/images/app-insights-3.png and /dev/null differ diff --git a/docs/images/app-service-settings-configuration.png b/docs/images/app-service-settings-configuration.png deleted file mode 100644 index 5887e1deb7..0000000000 Binary files a/docs/images/app-service-settings-configuration.png and /dev/null differ diff --git a/docs/images/configure-sql-1.png b/docs/images/configure-sql-1.png deleted file mode 100644 index 6792785447..0000000000 Binary files a/docs/images/configure-sql-1.png and /dev/null differ diff --git a/docs/images/configure-sql-2.png b/docs/images/configure-sql-2.png deleted file mode 100644 index cef2a5ff6e..0000000000 Binary files a/docs/images/configure-sql-2.png and /dev/null differ diff --git a/docs/images/diagnostic-settings-1.png b/docs/images/diagnostic-settings-1.png deleted file mode 100644 index fd73e0bf20..0000000000 Binary files a/docs/images/diagnostic-settings-1.png and /dev/null differ diff --git a/docs/images/diagnostic-settings-2.png b/docs/images/diagnostic-settings-2.png deleted file mode 100644 index c950d683e5..0000000000 Binary files a/docs/images/diagnostic-settings-2.png and /dev/null differ diff --git a/docs/images/diagnostic-settings-3.png b/docs/images/diagnostic-settings-3.png deleted file mode 100644 index b99f8b8a46..0000000000 Binary files a/docs/images/diagnostic-settings-3.png and /dev/null differ diff --git a/docs/images/dicom-cast-architecture.png b/docs/images/dicom-cast-architecture.png deleted file mode 100644 index 12c1e272bc..0000000000 Binary files a/docs/images/dicom-cast-architecture.png and /dev/null differ diff --git a/docs/images/dicom-deployment-architecture.png b/docs/images/dicom-deployment-architecture.png deleted file mode 100644 index e39e829756..0000000000 Binary files a/docs/images/dicom-deployment-architecture.png and /dev/null differ diff --git a/docs/images/dicomwebcontainer-bluecircle-copy-logs.png b/docs/images/dicomwebcontainer-bluecircle-copy-logs.png deleted file mode 100644 index 6ac133edc4..0000000000 Binary files a/docs/images/dicomwebcontainer-bluecircle-copy-logs.png and /dev/null differ diff --git a/docs/images/dicomwebcontainer-bluecircle-old-blob-format-dual.png b/docs/images/dicomwebcontainer-bluecircle-old-blob-format-dual.png deleted file mode 100644 index 85b5f45da5..0000000000 Binary files a/docs/images/dicomwebcontainer-bluecircle-old-blob-format-dual.png and /dev/null differ diff --git a/docs/images/dicomwebcontainer-bluecircle-old-blob-format.png b/docs/images/dicomwebcontainer-bluecircle-old-blob-format.png deleted file mode 100644 index 40f0955e01..0000000000 Binary files a/docs/images/dicomwebcontainer-bluecircle-old-blob-format.png and /dev/null differ diff --git a/docs/images/ohif-viewer-1.png b/docs/images/ohif-viewer-1.png deleted file mode 100644 index aead79f29c..0000000000 Binary files a/docs/images/ohif-viewer-1.png and /dev/null differ diff --git a/docs/images/ohif-viewer-2.png b/docs/images/ohif-viewer-2.png deleted file mode 100644 index 55765521eb..0000000000 Binary files a/docs/images/ohif-viewer-2.png and /dev/null differ diff --git a/docs/images/postman-singlepart-headers.PNG b/docs/images/postman-singlepart-headers.PNG deleted file mode 100644 index f31925d22e..0000000000 Binary files a/docs/images/postman-singlepart-headers.PNG and /dev/null differ diff --git a/docs/images/required-deployment.png b/docs/images/required-deployment.png deleted file mode 100644 index 35e66358d6..0000000000 Binary files a/docs/images/required-deployment.png and /dev/null differ diff --git a/docs/images/scale-out-1.png b/docs/images/scale-out-1.png deleted file mode 100644 index 4c0681d20f..0000000000 Binary files a/docs/images/scale-out-1.png and /dev/null differ diff --git a/docs/images/scale-out-2.png b/docs/images/scale-out-2.png deleted file mode 100644 index c3c42f7e7f..0000000000 Binary files a/docs/images/scale-out-2.png and /dev/null differ diff --git a/docs/images/scale-up-1.png b/docs/images/scale-up-1.png deleted file mode 100644 index 7b675b4abe..0000000000 Binary files a/docs/images/scale-up-1.png and /dev/null differ diff --git a/docs/images/scale-up-2.png b/docs/images/scale-up-2.png deleted file mode 100644 index 8e3f0c5792..0000000000 Binary files a/docs/images/scale-up-2.png and /dev/null differ diff --git a/docs/quickstarts/deploy-dicom-cast.md b/docs/quickstarts/deploy-dicom-cast.md deleted file mode 100644 index abb6596528..0000000000 --- a/docs/quickstarts/deploy-dicom-cast.md +++ /dev/null @@ -1,122 +0,0 @@ -# Deploy DICOM Cast - -In this quickstart, you will learn how to deploy DICOM Cast for your Medical Imaging Server for DICOM. DICOM Cast is a service which pushes Medical Imaging Server for DICOM metadata into a FHIR™ server to support integrated queries across clinical and imaging data. To learn more about the architecture of DICOM Cast, refer to the [DICOM Cast Concept](../concepts/dicom-cast.md). - -By competing this quickstart, you will have deployed DICOM Cast for your Medical Imaging Server for DICOM. To learn how to sync your Medical Imaging Server for DICOM metadata into a FHIR™ server, see [Sync DICOM Metadata to FHIR™ with DICOM Cast](../how-to-guides/sync-dicom-metadata-to-fhir.md). - -## Deploy via Docker - -If you would like to deploy via docker-compose, please follow the quickstart [Deploy Via Docker](deploy-via-docker.md). This will deploy the Medical Imaging Server for DICOM and DICOM Cast. - -## Deploy via Azure with Azure Healthcare APIs - -DICOM Cast can be deployed as an Azure Container Instance targeting an Azure Healthcare API Workspace with a FHIR Service and a DICOM Service. using the provided [ARM template](/converter/dicom-cast/samples/templates/deploy-with-healthcareapis.json). - -> Note: Currently DICOM Cast only supports R4 of FHIR. - -### Deployment - -If you have an Azure subscription, click the link below to deploy to Azure: - - - - - -If you do not have an Azure subscription, create a [free account](https://azure.microsoft.com/free) before you begin. - -This will deploy the following resources to the specified resource group: - -* Azure Container Instance - + Used to run the DICOM Cast executable - + The image used is specified via the `image` parameter and defaults to the latest CI build - + A managed identity is also configured -* Application Insights - + Used to log events and errors generated by the Medical Imaging Server for DICOM - + If `deployApplicationInsights` is specified, an Application Insights instance is deployed for logging -* Storage Account - + Used to keep track of the state of the service - + Persists information about exceptions in table storage -* KeyVault - + Used to store the storage connection string - + Is accessed via the managed identity specified on ACI -* Azure Healthcare APIs Workspace -* R4 FHIR Service -* DICOM Service -* Role assignments of `DICOM Data Owner` and `FHIR Data Contributor` to the ACI managed identity. - -Instructions for how to deploy an ARM template can be found in the following docs -* [Deploy via Portal](https://docs.microsoft.com/azure/azure-resource-manager/templates/deploy-portal) -* [Deploy via CLI](https://docs.microsoft.com/azure/azure-resource-manager/templates/deploy-cli) -* [Deploy via PowerShell](https://docs.microsoft.com/azure/azure-resource-manager/templates/deploy-powershell) - -## Deploy via Azure with OSS - -DICOM Cast can be deployed as an Azure Container Instance using the provided [ARM template](/converter/dicom-cast/samples/templates/default-azuredeploy.json). - -### Prerequisites - -* A deployed [FHIR Server for Azure](https://github.com/microsoft/fhir-server) -* A deployed [Medical Imaging Server for DICOM](https://github.com/microsoft/dicom-server) - -> Note: Currently DICOM Cast is only supported by the Open Source FHIR Server for Azure or the Azure Healthcare APIs FHIR Service, not the Azure API for FHIR. Ensure that the FHIR server is on version R4 of FHIR. - -### Deployment - -If you have an Azure subscription, click the link below to deploy to Azure: - - - - - -If you do not have an Azure subscription, create a [free account](https://azure.microsoft.com/free) before you begin. - -This will deploy the following resources to the specified resource group: - -* Azure Container Instance - + Used to run the DICOM Cast executable - + The image used is specified via the `image` parameter and defaults to the latest CI build - + A managed identity is also configured -* Application Insights - + Used to log events and errors generated by the Medical Imaging Server for DICOM - + If `deployApplicationInsights` is specified, an Application Insights instance is deployed for logging -* Storage Account - + Used to keep track of the state of the service - + Persists information about exceptions in table storage -* KeyVault - + Used to store the storage connection string - + Is accessed via the managed identity specified on ACI - -Instructions for how to deploy an ARM template can be found in the following docs -* [Deploy via Portal](https://docs.microsoft.com/azure/azure-resource-manager/templates/deploy-portal) -* [Deploy via CLI](https://docs.microsoft.com/azure/azure-resource-manager/templates/deploy-cli) -* [Deploy via PowerShell](https://docs.microsoft.com/azure/azure-resource-manager/templates/deploy-powershell) - -#### Deploy DICOM Server, FHIR Server and DICOM Cast - -If you have not yet deployed a DICOM or FHIR Server, you can use use a single ARM Template to deploy the Medical Imaging Server for DICOM, a FHIR OSS Server and a DICOM Cast instance at once. If you do not yet have an Azure subscription, create a [free account](https://azure.microsoft.com/free) before deploying. - -For a simplified quick deploy, click the link below to deploy to Azure: - - - - - -> NOTE: this option is ideal to quickly set up DICOM Cast. If you want to customize configuration, you will need to do so after deployment. - -If you want to customize all DICOM, FHIR and DICOM Cast settings during deployment, click the link below to deploy to Azure: - - - - - -Both of these ARM templates will deploy: - -* Medical Imaging Server for DICOM -* [FHIR OSS Server](https://github.com/microsoft/fhir-server) -* DICOM Cast - -These three instances will be hosted within the same Resource Group and App Service Plan. - -## Summary - -In this Quickstart, you will learned how to deploy DICOM Cast for your Medical Imaging Server for DICOM. Reference the [DICOM Cast Concept](../concepts/dicom-cast.md) to learn more. To start using DICOM Cast, see [Sync DICOM Metadata to FHIR™ with DICOM Cast](../how-to-guides/sync-dicom-metadata-to-fhir.md). To configure DICOM Cast with Private Link enabled DICOM Service and FHIR Service, see the instructions here: [DICOM Cast Configuration with Private Link enabled DICOM and FHIR](../../converter/dicom-cast/docs/workingWithPrivateLink.md) diff --git a/docs/quickstarts/deploy-via-azure.md b/docs/quickstarts/deploy-via-azure.md deleted file mode 100644 index dd901e3036..0000000000 --- a/docs/quickstarts/deploy-via-azure.md +++ /dev/null @@ -1,44 +0,0 @@ -# Deploy the Medical Imaging Server for DICOM using the Azure portal - -In this quickstart, you'll learn how to deploy the Medical Imaging Server for DICOM using the Azure portal. - -If you do not have an Azure subscription, create a [free account](https://azure.microsoft.com/free) before you begin. - -Once you have your subscription, click the button below to begin deployment:
- - -Instructions for how to deploy an ARM template can be found in the following docs -* [Deploy via Portal](https://docs.microsoft.com/azure/azure-resource-manager/templates/deploy-portal) -* [Deploy via CLI](https://docs.microsoft.com/azure/azure-resource-manager/templates/deploy-cli) -* [Deploy via PowerShell](https://docs.microsoft.com/azure/azure-resource-manager/templates/deploy-powershell) - -## Enter account details - -1. Select your Azure subscription. -1. Select an existing resource group or create a new one. -1. Select the region to deploy your Medical Imaging Server for DICOM. -1. Select a Service Name for your deployment. Note that the Service Name will be included in the URL you will use to access the application. - -![required-deployment-config](../images/required-deployment.png) - -## Configure deployment settings - -Configure the remaining deployment settings for your Medical Imaging Server. The default settings are appropriate for an excellent dev/test or proof-of-concept environment as they are inexpensive, yet perform well for small to medium loads. For a production environment, upgrading to regionally redundant storage, failover databases and autoscaling application servers is recommended. - -Refer to [Configure Medical Imaging Server for DICOM](../how-to-guides/configure-dicom-server-settings.md) for more configuration instructions. - -> NOTE: Refer to the [SQL Server Password Complexity Requirements](https://docs.microsoft.com/sql/relational-databases/security/password-policy?view=sql-server-ver15 -) when setting your SQL admin password. - -## Summary - -In this quickstart, you learned how to deploy and configure the Medical Imaging Server for DICOM using the Azure portal. - -Once deployment is complete, you can use the Azure Portal to navigate to the newly created App Service to see the details. The default URL to access your Medical Imaging Server for DICOM will be: ```https://.azurewebsites.net```. Make sure to specify the version as part of the url when making requests. More information can be found in the [Api Versioning Documentation](../api-versioning.md) - -To get started using your newly deployed Medical Imaging Server for DICOM, refer to the following documents: - -* [Configure Medical Imaging Server for DICOM](../how-to-guides/configure-dicom-server-settings.md) -* [Use Medical Imaging Server for DICOM APIs](../tutorials/use-the-medical-imaging-server-apis.md) -* [Upload DICOM files via the Electron Tool](../../tools/dicom-web-electron) -* [Enable Azure AD Authentication](../how-to-guides/enable-authentication-with-tokens.md) diff --git a/docs/quickstarts/deploy-via-docker.md b/docs/quickstarts/deploy-via-docker.md deleted file mode 100644 index 13e48c640d..0000000000 --- a/docs/quickstarts/deploy-via-docker.md +++ /dev/null @@ -1,83 +0,0 @@ -# Deploy the Medical Imaging Server for DICOM locally using Docker - -This quickstart guide details how to build and run the Medical Imaging Server for DICOM in Docker. By using Docker Compose, all of the necessary dependencies are started automatically in containers without requiring any installations on your development machine. In particular, the Medical Imaging Server for DICOM in Docker starts a container for [SQL Server](https://docs.microsoft.com/sql/linux/quickstart-install-connect-docker?view=sql-server-ver15&pivots=cs1-bash) and the Azure Storage emulator called [Azurite](https://github.com/Azure/Azurite). - -> **IMPORTANT** -> -> This sample has been created to enable Development/Test scenarios and is not suitable for production scenarios. Passwords are contained in deployment files, the SQL server connection is not encrypted, authentication on the Medical Imaging Server for DICOM has been disabled, and data is not persisted between container restarts. - -## Visual Studio (DICOM Server Only) - -You can easily run and debug the Medical Imaging Server for DICOM right from Visual Studio. Simply open up the solution file *Microsoft.Health.Dicom.sln* in Visual Studio 2019 (or later) and run the "docker-compose" project. This should build each of the images and run the containers locally without any additional action. - -Once it's ready, a web page should open automatically for the URL `https://localhost:8080` where you can communicate with the Medical Imaging Server for DICOM. - -## Command Line - -Run the following command from the root of the `microsoft/dicom-server` repository, replacing `` with your chosen password (be sure to follow the [SQL Server password complexity requirements](https://docs.microsoft.com/sql/relational-databases/security/password-policy?view=sql-server-ver15#password-complexity)): - -```bash -docker-compose -p healthcare -f docker/docker-compose.yml up --build -d -``` - -If you wish to specify your own SQL admin password, you can include one as well: - -```bash -env SAPASSWORD='' docker-compose -p healthcare -f docker/docker-compose.yml up --build -d -``` - -Once deployed the Medical Imaging Server for DICOM should be available at `http://localhost:8080/`. - -### Including DICOMcast - -If you also want to include DICOMcast, simply add one more file to the `docker-compose up` command: - -```bash -docker-compose -p healthcare -f docker/docker-compose.yml -f docker/docker-compose.cast.yml up --build -d -``` - -### Run in Docker with a custom configuration - -To build the `dicom-server` image run the following command from the root of the `microsoft/dicom-server`repository: - -```bash -docker build -f src/microsoft.health.dicom.web/Dockerfile -t dicom-server . -``` - -When running the container, additional configuration details can also be specified such as: - -```bash -docker run -d \ - -e DicomServer__Security__Enabled="false" \ - -e SqlServer__ConnectionString="Server=tcp:,1433;Initial Catalog=Dicom;Persist Security Info=False;User ID=sa;Password=;MultipleActiveResultSets=False;Connection Timeout=30;TrustServerCertificate=true" \ - -e SqlServer__AllowDatabaseCreation="true" \ - -e SqlServer__Initialize="true" \ - -e BlobStore__ConnectionString="" \ - -p 8080:8080 \ - dicom-server -``` - -## Connecting to Dependencies - -By default, the storage services like `azurite` and `sql` are not exposed locally, but you may connect to them directly by uncommenting the `ports` element in the `docker-compose.yml` file. Be sure those ports aren't already in-use locally! Without changing the values, the following ports are used: -* SQL Server exposes a TCP connection on port `1433` - * In a SQL connection string, use `localhost:1433` or even `tcp:(local)` -* Azurite, the Azure Storage Emulator, exposes the blob service on port `10000`, the queue service on port `10001`, and the table service on port `10002` - * The emulator uses a well-defined [connection string](https://docs.microsoft.com/en-us/azure/storage/common/storage-use-emulator#connect-to-the-emulator-account-using-the-well-known-account-name-and-key) - * Use [Azure Storage Explorer](https://azure.microsoft.com/features/storage-explorer/) to browse its contents -* [FHIR](https://github.com/microsoft/fhir-server) can be accessible via `http://localhost:8081` - -You can also connect to them via their IP address rather rather than via localhost. The following command will help you understand the IPs and ports by which the services are exposed: - -```bash -docker inspect -f 'Name: {{.Name}} - IPs: {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}} - Ports: {{.Config.ExposedPorts}}' $(docker ps -aq) -``` - -## Next steps - -Once deployment is complete you can access your Medical Imaging Server at `https://localhost:8080`. Make sure to specify the version as part of the url when making requests. More information can be found in the [Api Versioning Documentation](../api-versioning.md) - -* [Use Medical Imaging Server for DICOM APIs](../tutorials/use-the-medical-imaging-server-apis.md) -* [Upload DICOM files via the Electron Tool](../../tools/dicom-web-electron) -* [Enable Azure AD Authentication](../how-to-guides/enable-authentication-with-tokens.md) -* [Enable Identity Server Authentication](../development/identity-server-authentication.md) diff --git a/docs/resources/Conformance-as-Postman.postman_collection.json b/docs/resources/Conformance-as-Postman.postman_collection.json deleted file mode 100644 index 926a1a583e..0000000000 --- a/docs/resources/Conformance-as-Postman.postman_collection.json +++ /dev/null @@ -1,1740 +0,0 @@ -{ - "info": { - "_postman_id": "2d4290a7-8227-4a7a-8913-181a2372848f", - "name": "Conformance-as-Postman", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" - }, - "item": [ - { - "name": "Store-single-instance (red-triangle.dcm)", - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "id": "0ed3341a-b4e6-4bdb-a8cc-b9fdcb0a7fa9" - } - } - ], - "protocolProfileBehavior": { - "disabledSystemHeaders": { - "accept": true, - "accept-encoding": true, - "connection": true, - "content-type": true, - "user-agent": true - } - }, - "request": { - "method": "POST", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/dicom+json" - }, - { - "key": "Content-Type", - "type": "text", - "value": "application/dicom" - } - ], - "body": { - "mode": "file", - "file": { - "src": "/C:/githealth/dicom-server/docs/dcms/red-triangle.dcm" - }, - "options": { - "raw": { - "language": "text" - } - } - }, - "url": { - "raw": "{{optionallyPartitionedUrl}}/studies", - "host": [ - "{{optionallyPartitionedUrl}}" - ], - "path": [ - "studies" - ] - }, - "description": "For the body of the request, select the red-triangle.dcm file located in the GitHub repo at ../docs/dcms. Ensure you attach the file as `binary`.\r\n\r\n> NOTE: This is a non-standard API that allows the upload of a single DICOM file without the need to configure the POST for multipart/related. It allows the use of Postman to upload files to the DICOMweb service.\r\n\r\nThe following is required to upload a single DICOM file.\r\n\r\n* Path: ../studies\r\n* Method: POST\r\n* Headers:\r\n * `Accept: application/dicom+json`\r\n * `Content-Type: application/dicom`\r\n* Body:\r\n * Contains the DICOM file as a bytes.\r\n\r\n> This API is currently not implemented\r\n" - }, - "response": [] - }, - { - "name": "Store-single-instance (green-square.dcm)", - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "id": "3c48a308-2eba-455c-8e75-564b5bf5119d" - } - } - ], - "protocolProfileBehavior": { - "disabledSystemHeaders": { - "accept": true, - "accept-encoding": true, - "connection": true, - "content-type": true, - "user-agent": true - } - }, - "request": { - "method": "POST", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/dicom+json" - }, - { - "key": "Content-Type", - "type": "text", - "value": "application/dicom" - } - ], - "body": { - "mode": "file", - "file": { - "src": "/C:/githealth/dicom-server/docs/dcms/green-square.dcm" - }, - "options": { - "raw": { - "language": "text" - } - } - }, - "url": { - "raw": "{{optionallyPartitionedUrl}}/studies", - "host": [ - "{{optionallyPartitionedUrl}}" - ], - "path": [ - "studies" - ] - }, - "description": "This upload is simply to ensure the remaining requests succeed. Ideally, red-triangle.dcm, blue-cirle.dcm, and green-square.dcm are all uploaded.\r\n\r\nFor the body of the request, select the green-square.dcm file located in the GitHub repo at ../docs/dcms. Ensure you attach the file as `binary`.\r\n\r\n> NOTE: This is a non-standard API that allows the upload of a single DICOM file without the need to configure the POST for multipart/related. It allows the use of Postman to upload files to the DICOMweb service.\r\n\r\nThe following is required to upload a single DICOM file.\r\n\r\n* Path: ../studies\r\n* Method: POST\r\n* Headers:\r\n * `Accept: application/dicom+json`\r\n * `Content-Type: application/dicom`\r\n* Body:\r\n * Contains the DICOM file as a bytes.\r\n\r\n> This API is currently not implemented\r\n" - }, - "response": [] - }, - { - "name": "Store-single-instance (blue-circle.dcm)", - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "id": "7d495a64-8b32-48dc-90d9-2c0122d145e6" - } - } - ], - "protocolProfileBehavior": { - "disabledSystemHeaders": { - "accept": true, - "accept-encoding": true, - "connection": true, - "content-type": true, - "user-agent": true - } - }, - "request": { - "method": "POST", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/dicom+json" - }, - { - "key": "Content-Type", - "type": "text", - "value": "application/dicom" - } - ], - "body": { - "mode": "file", - "file": { - "src": "/C:/githealth/dicom-server/docs/dcms/blue-circle.dcm" - }, - "options": { - "raw": { - "language": "text" - } - } - }, - "url": { - "raw": "{{optionallyPartitionedUrl}}/studies", - "host": [ - "{{optionallyPartitionedUrl}}" - ], - "path": [ - "studies" - ] - }, - "description": "This upload is simply to ensure the remaining requests succeed. Ideally, red-triangle.dcm, blue-cirle.dcm, and green-square.dcm are all uploaded.\r\n\r\nFor the body of the request, select the blue-circle.dcm file located in the GitHub repo at ../docs/dcms. Ensure you attach the file as `binary`.\r\n\r\n> NOTE: This is a non-standard API that allows the upload of a single DICOM file without the need to configure the POST for multipart/related. It allows the use of Postman to upload files to the DICOMweb service.\r\n\r\nThe following is required to upload a single DICOM file.\r\n\r\n* Path: ../studies\r\n* Method: POST\r\n* Headers:\r\n * `Accept: application/dicom+json`\r\n * `Content-Type: application/dicom`\r\n* Body:\r\n * Contains the DICOM file as a bytes.\r\n\r\n> This API is currently not implemented\r\n" - }, - "response": [] - }, - { - "name": "[will not work - see description] Store-instances-using-multipart/related", - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "id": "5631a407-6ae7-4f19-a03b-959adefdb26a" - } - } - ], - "protocolProfileBehavior": { - "disabledSystemHeaders": { - "accept": true, - "accept-encoding": true, - "connection": true, - "content-type": true, - "user-agent": true - } - }, - "request": { - "method": "POST", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/dicom+json" - }, - { - "key": "Content-Type", - "type": "text", - "value": "multipart/related;boundary=ABCD1234" - } - ], - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "file", - "contentType": "application/dicom", - "type": "file", - "src": "/C:/githealth/dicom-samples/visus.com/case1/dicomfile" - } - ], - "options": { - "raw": { - "language": "text" - } - } - }, - "url": { - "raw": "{{optionallyPartitionedUrl}}/studies", - "host": [ - "{{optionallyPartitionedUrl}}" - ], - "path": [ - "studies" - ] - }, - "description": "This request intends to demonstrate how to upload DICOM files using multipart/related. However, it will not work in Postman.\r\n\r\n> NOTE: Postman cannot be used to upload DICOM files in a way that complies with the DICOM standard. This is due to a Postman limitation. Instead, consider using cURL or programatically uploading the file using Python, C# or another full featured language.\r\n\r\n_Details:_\r\n\r\n* Path: ../studies\r\n* Method: POST\r\n* Headers:\r\n * `Accept: application/dicom+json`\r\n * `Content-Type: multipart/related; type=\"application/dicom\"`\r\n* Body:\r\n * `Content-Type: application/dicom` for each file uploaded, separated by a boundary value\r\n\r\n> Some programming languages and tools behave differently. For instance, some require you to define your own boundary. For those, you may need to use a slightly modified Content-Type header. The following have been used successfully.\r\n> * `Content-Type: multipart/related; type=\"application/dicom\"; boundary=ABCD1234`\r\n> * `Content-Type: multipart/related; boundary=ABCD1234`\r\n> * `Content-Type: multipart/related`\r\n\r\nIf using Postman, please consider using Store-single-instance. This is a non-standard API that allows the upload of a single DICOM file without the need to configure the POST for multipart/related." - }, - "response": [] - }, - { - "name": "[will not work - see description] Store-instances-for-a-specific-study", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "formdata", - "formdata": [] - }, - "url": { - "raw": "{{optionallyPartitionedUrl}}/studies/{{study1}}", - "host": [ - "{{optionallyPartitionedUrl}}" - ], - "path": [ - "studies", - "{{study1}}" - ] - }, - "description": "This request intends to demonstrate how to upload DICOM files using multipart/related to a designated study. However, it will not work in Postman.\r\n\r\n> NOTE: Postman cannot be used to upload DICOM files in a way that complies with the DICOM standard. This is due to a Postman limitation. Instead, consider using cURL or programatically uploading the file using Python, C# or another full featured language.\r\n\r\n_Details:_\r\n* Path: ../studies/{study}\r\n* Method: POST\r\n* Headers:\r\n * `Accept: application/dicom+json`\r\n * `Content-Type: multipart/related; type=\"application/dicom\"`\r\n* Body:\r\n * `Content-Type: application/dicom` for each file uploaded, separated by a boundary value\r\n\r\n> Some programming languages and tools behave differently. For instance, some require you to define your own boundary. For those, you may need to use a slightly modified Content-Type header. The following have been used successfully.\r\n > * `Content-Type: multipart/related; type=\"application/dicom\"; boundary=ABCD1234`\r\n > * `Content-Type: multipart/related; boundary=ABCD1234`\r\n > * `Content-Type: multipart/related`\r\n\r\nIf using Postman, please consider using Store-single-instance. This is a non-standard API that allows the upload of a single DICOM file without the need to configure the POST for multipart/related." - }, - "response": [] - }, - { - "name": "Retrieve-all-instances-within-a-study", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Status code is 200\", function () {\r", - " pm.response.to.have.status(200);\r", - "});\r", - "" - ], - "type": "text/javascript", - "id": "3f4906ad-42eb-454c-8887-ef8e808d87bb" - } - } - ], - "protocolProfileBehavior": { - "disabledSystemHeaders": { - "accept": true - } - }, - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "multipart/related; type=\"application/dicom\"; transfer-syntax=*", - "type": "text" - } - ], - "url": { - "raw": "{{optionallyPartitionedUrl}}/studies/{{study1}}", - "host": [ - "{{optionallyPartitionedUrl}}" - ], - "path": [ - "studies", - "{{study1}}" - ] - }, - "description": "This request retrieves all instances within a single study, and returns them as a collection of multipart/related bytes.\r\n\r\n_Details:_\r\n* Path: ../studies/{study}\r\n* Method: GET\r\n* Headers:\r\n * `Accept: multipart/related; type=\"application/dicom\"; transfer-syntax=*`\r\n" - }, - "response": [] - }, - { - "name": "Retrieve-metadata-of-all-instances-in-study", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Status code is 200\", function () {\r", - " pm.response.to.have.status(200);\r", - "});\r", - "\r", - "let seriesid = pm.variables.get(\"series1\");\r", - "pm.test(\"Series in returned metadata\", function () {\r", - " pm.expect(pm.response.text()).to.include(seriesid);\r", - "});" - ], - "type": "text/javascript", - "id": "78acdc8d-4e85-4d86-b715-bfb87175fe57" - } - } - ], - "protocolProfileBehavior": { - "disabledSystemHeaders": { - "accept": true - } - }, - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/dicom+json", - "type": "text" - } - ], - "url": { - "raw": "{{optionallyPartitionedUrl}}/studies/{{study1}}/metadata", - "host": [ - "{{optionallyPartitionedUrl}}" - ], - "path": [ - "studies", - "{{study1}}", - "metadata" - ] - }, - "description": "This request retrieves the metadata for all instances within a single study.\r\n\r\n_Details:_\r\n* Path: ../studies/{study}/metadata\r\n* Method: GET\r\n* Headers:\r\n * `Accept: application/dicom+json`\r\n" - }, - "response": [] - }, - { - "name": "Retrieve-all-instances-within-a-series", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Status code is 200\", function () {\r", - " pm.response.to.have.status(200);\r", - "});" - ], - "type": "text/javascript", - "id": "76147669-8206-4c96-b1b3-ae4428ddc7fb" - } - } - ], - "protocolProfileBehavior": { - "disabledSystemHeaders": { - "accept": true - } - }, - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "multipart/related; type=\"application/dicom\"; transfer-syntax=*", - "type": "text" - } - ], - "url": { - "raw": "{{optionallyPartitionedUrl}}/studies/{{study1}}/series/{{series1}}", - "host": [ - "{{optionallyPartitionedUrl}}" - ], - "path": [ - "studies", - "{{study1}}", - "series", - "{{series1}}" - ] - }, - "description": "This request retrieves all instances within a single series, and returns them as a collection of multipart/related bytes.\r\n\r\n_Details:_\r\n* Path: ../studies/{study}/series{series}\r\n* Method: GET\r\n* Headers:\r\n * `Accept: multipart/related; type=\"application/dicom\"; transfer-syntax=*`\r\n" - }, - "response": [] - }, - { - "name": "Retrieve-metadata-of-all-instances-within-a-series", - "protocolProfileBehavior": { - "disabledSystemHeaders": { - "accept": true - } - }, - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/dicom+json", - "type": "text" - } - ], - "url": { - "raw": "{{optionallyPartitionedUrl}}/studies/{{study1}}/series/{{series1}}/metadata", - "host": [ - "{{optionallyPartitionedUrl}}" - ], - "path": [ - "studies", - "{{study1}}", - "series", - "{{series1}}", - "metadata" - ] - }, - "description": "This request retrieves the metadata for all instances within a single series.\r\n\r\n_Details:_\r\n* Path: ../studies/{study}/series/{series}/metadata\r\n* Method: GET\r\n* Headers:\r\n * `Accept: application/dicom+json`\r\n" - }, - "response": [] - }, - { - "name": "Retrieve-a-single-instance-within-a-series-of-a-study", - "protocolProfileBehavior": { - "disabledSystemHeaders": { - "accept": true - } - }, - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/dicom; transfer-syntax=*", - "type": "text" - }, - { - "key": "", - "value": "", - "type": "text", - "disabled": true - } - ], - "url": { - "raw": "{{optionallyPartitionedUrl}}/studies/{{study1}}/series/{{series1}}/instances/{{instance1}}", - "host": [ - "{{optionallyPartitionedUrl}}" - ], - "path": [ - "studies", - "{{study1}}", - "series", - "{{series1}}", - "instances", - "{{instance1}}" - ] - }, - "description": "This request retrieves a single instances, and returns it as a DICOM formatted stream of bytes.\r\n\r\n_Details:_\r\n* Path: ../studies/{study}/series{series}/instances/{instance}\r\n* Method: GET\r\n* Headers:\r\n * `Accept: application/dicom; transfer-syntax=*`\r\n" - }, - "response": [] - }, - { - "name": "Retrieve-metadata-of-a-single-instance-within-a-series-of-a-study", - "protocolProfileBehavior": { - "disabledSystemHeaders": { - "accept": true - } - }, - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/dicom+json", - "type": "text" - } - ], - "url": { - "raw": "{{optionallyPartitionedUrl}}/studies/{{study1}}/series/{{series1}}/instances/{{instance1}}/metadata", - "host": [ - "{{optionallyPartitionedUrl}}" - ], - "path": [ - "studies", - "{{study1}}", - "series", - "{{series1}}", - "instances", - "{{instance1}}", - "metadata" - ] - }, - "description": "This request retrieves the metadata for a single instances within a single study and series.\r\n\r\n_Details:_\r\n* Path: ../studies/{study}/series/{series}/instances/{instance}/metadata\r\n* Method: GET\r\n* Headers:\r\n * `Accept: application/dicom+json`" - }, - "response": [] - }, - { - "name": "Retrieve-rendered-image-of-a-single-instance-within-a-series", - "protocolProfileBehavior": { - "disabledSystemHeaders": { - "accept": true - } - }, - "request": { - "method": "GET", - "header": [ - { - "description": "Either application/jpeg or application/png. If unspecified default to application/jpeg", - "key": "Accept", - "type": "text", - "value": "application/jpeg" - } - ], - "url": { - "raw": "{{optionallyPartitionedUrl}}/studies/{{study1}}/series/{{series1}}/instances/{{instance1}}/rendered?quality=100", - "host": [ - "{{optionallyPartitionedUrl}}" - ], - "path": [ - "studies", - "{{study1}}", - "series", - "{{series1}}", - "instances", - "{{instance1}}", - "rendered" - ], - "query": [ - { - "key": "quality", - "value": "100", - "description": "Quality for jpeg request, can be 1-100 inclusive. Ignored if requesting a png" - } - ] - }, - "description": "This request retrieves a rendered image for a single instance within a single study and series. If there are multiple frames in the instance then the first frame is rendered as default.\r\n\r\n_Details:_\r\n* Path: ../studies/{study}/series/{series}/instances/{instance}/rendered\r\n* Method: GET\r\n* Headers:\r\n * `Accept: application/jpeg`\r\n * `Accept: application/png`\r\n* Query Paramaters\r\n * `quality: 1-100`" - }, - "response": [] - }, - { - "name": "Retrieve-rendered-image-of-a-single-frame-within-a-instance", - "protocolProfileBehavior": { - "disabledSystemHeaders": { - "accept": true - } - }, - "request": { - "method": "GET", - "header": [ - { - "description": "Either application/jpeg or application/png. If unspecified default to application/jpeg", - "key": "Accept", - "type": "text", - "value": "application/jpeg" - } - ], - "url": { - "raw": "{{optionallyPartitionedUrl}}/studies/{{study1}}/series/{{series1}}/instances/{{instance1}}/frames/{{frame}}/rendered?quality=100", - "host": [ - "{{optionallyPartitionedUrl}}" - ], - "path": [ - "studies", - "{{study1}}", - "series", - "{{series1}}", - "instances", - "{{instance1}}", - "frames", - "{{frame}}", - "rendered" - ], - "query": [ - { - "key": "quality", - "value": "100", - "description": "Quality for jpeg request, can be 1-100 inclusive. Ignored if requesting a png" - } - ] - }, - "description": "This request retrieves a rendered image for a single frame within a single instance. Only supports one frame.\r\n\r\n_Details:_\r\n* Path: ../studies/{study}/series/{series}/instances/{instance}/frames/{frame}/rendered\r\n* Method: GET\r\n* Headers:\r\n * `Accept: application/jpeg`\r\n * `Accept: application/png`\r\n* Query Paramaters\r\n * `quality: 1-100`" - }, - "response": [] - }, - { - "name": "Retrieve-one-or-more-frames-from-a-single-instance", - "protocolProfileBehavior": { - "disabledSystemHeaders": { - "accept": true - } - }, - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "multipart/related; type=\"application/octet-stream\"; transfer-syntax=1.2.840.10008.1.2.1", - "type": "text" - } - ], - "url": { - "raw": "{{optionallyPartitionedUrl}}/studies/{{study1}}/series/{{series1}}/instances/{{instance1}}/frames/1", - "host": [ - "{{optionallyPartitionedUrl}}" - ], - "path": [ - "studies", - "{{study1}}", - "series", - "{{series1}}", - "instances", - "{{instance1}}", - "frames", - "1" - ] - }, - "description": "This request retrieves one or more frames from a single instance, and returns them as a collection of multipart/related bytes.\r\n\r\n_Details:_\r\n* Path: ../studies/{study}/series{series}/instances/{instance}\r\n* Method: GET\r\n* Headers:\r\n * `Accept: multipart/related; type=\"application/octet-stream\"; transfer-syntax=1.2.840.10008.1.2.1` (Default) or\r\n * `Accept: multipart/related; type=\"application/octet-stream\"; transfer-syntax=*` or\r\n * `Accept: multipart/related; type=\"application/octet-stream\";`\r\n" - }, - "response": [] - }, - { - "name": "Search-for-studies", - "protocolProfileBehavior": { - "disabledSystemHeaders": { - "accept": true - } - }, - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/dicom+json" - } - ], - "url": { - "raw": "{{optionallyPartitionedUrl}}/studies?StudyInstanceUID={{study1}}", - "host": [ - "{{optionallyPartitionedUrl}}" - ], - "path": [ - "studies" - ], - "query": [ - { - "key": "StudyInstanceUID", - "value": "{{study1}}" - } - ] - }, - "description": "This request enables searches for studies, series and instances by DICOM attributes.\r\n\r\n> Please see the [Conformance.md](https://github.com/microsoft/dicom-server/blob/main/docs/users/Conformance.md) file for supported DICOM attributes.\r\n\r\n_Details:_\r\n* Path: ../studies?StudyInstanceUID={{study1}}\r\n* Method: GET\r\n* Headers:\r\n * `Accept: application/dicom+json`\r\n" - }, - "response": [] - }, - { - "name": "Search-for-series", - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/dicom+json", - "type": "text" - } - ], - "url": { - "raw": "{{optionallyPartitionedUrl}}/series?SeriesInstanceUID={{series1}}", - "host": [ - "{{optionallyPartitionedUrl}}" - ], - "path": [ - "series" - ], - "query": [ - { - "key": "SeriesInstanceUID", - "value": "{{series1}}" - } - ] - }, - "description": "This request enables searches for one or more series by DICOM attributes.\r\n\r\n> Please see the [Conformance.md](https://github.com/microsoft/dicom-server/blob/main/docs/users/Conformance.md) file for supported DICOM attributes.\r\n\r\n_Details:_\r\n* Path: ../series?SeriesInstanceUID={{series}}\r\n* Method: GET\r\n* Headers:\r\n * `Accept: application/dicom+json`\r\n" - }, - "response": [] - }, - { - "name": "Search-for-series-within-a-study", - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/dicom+json", - "type": "text" - } - ], - "url": { - "raw": "{{optionallyPartitionedUrl}}/studies/{{study1}}/series?SeriesInstanceUID={{series1}}", - "host": [ - "{{optionallyPartitionedUrl}}" - ], - "path": [ - "studies", - "{{study1}}", - "series" - ], - "query": [ - { - "key": "SeriesInstanceUID", - "value": "{{series1}}" - } - ] - }, - "description": "This request enables searches for one or more series within a single study by DICOM attributes.\r\n\r\n> Please see the [Conformance.md](https://github.com/microsoft/dicom-server/blob/main/docs/users/Conformance.md) file for supported DICOM attributes.\r\n\r\n_Details:_\r\n* Path: ../studies/{{study}}/series?SeriesInstanceUID={{series}}\r\n* Method: GET\r\n* Headers:\r\n * `Accept: application/dicom+json`\r\n" - }, - "response": [] - }, - { - "name": "Search-for-instances", - "protocolProfileBehavior": { - "disabledSystemHeaders": { - "accept": true - } - }, - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/dicom+json", - "type": "text" - } - ], - "url": { - "raw": "{{optionallyPartitionedUrl}}/instances?SOPInstanceUID={{instance1}}", - "host": [ - "{{optionallyPartitionedUrl}}" - ], - "path": [ - "instances" - ], - "query": [ - { - "key": "SOPInstanceUID", - "value": "{{instance1}}" - } - ] - }, - "description": "This request enables searches for one or more instances by DICOM attributes.\r\n\r\n> Please see the [Conformance.md](https://github.com/microsoft/dicom-server/blob/main/docs/users/Conformance.md) file for supported DICOM attributes.\r\n\r\n_Details:_\r\n* Path: ../instances?SOPInstanceUID={{instance}}\r\n* Method: GET\r\n* Headers:\r\n * `Accept: application/dicom+json`\r\n" - }, - "response": [] - }, - { - "name": "Search-for-instances-within-a-study", - "protocolProfileBehavior": { - "disabledSystemHeaders": { - "accept": true - } - }, - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/dicom+json", - "type": "text" - } - ], - "url": { - "raw": "{{optionallyPartitionedUrl}}/studies/{{study1}}/instances?SOPInstanceUID={{instance1}}", - "host": [ - "{{optionallyPartitionedUrl}}" - ], - "path": [ - "studies", - "{{study1}}", - "instances" - ], - "query": [ - { - "key": "SOPInstanceUID", - "value": "{{instance1}}" - } - ] - }, - "description": "This request enables searches for one or more instances within a single study by DICOM attributes.\r\n\r\n> Please see the [Conformance.md](https://github.com/microsoft/dicom-server/blob/main/docs/users/Conformance.md) file for supported DICOM attributes.\r\n\r\n_Details:_\r\n* Path: ../studies/{{study}}/instances?SOPInstanceUID={{instance}}\r\n* Method: GET\r\n* Headers:\r\n * `Accept: application/dicom+json`" - }, - "response": [] - }, - { - "name": "Search-for-instances-within-a-study-and-series", - "protocolProfileBehavior": { - "disabledSystemHeaders": { - "accept": true - } - }, - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/dicom+json" - } - ], - "url": { - "raw": "{{optionallyPartitionedUrl}}/studies/{{study1}}/series/{{series1}}/instances?SOPInstanceUID={{instance1}}", - "host": [ - "{{optionallyPartitionedUrl}}" - ], - "path": [ - "studies", - "{{study1}}", - "series", - "{{series1}}", - "instances" - ], - "query": [ - { - "key": "SOPInstanceUID", - "value": "{{instance1}}" - } - ] - }, - "description": "This request enables searches for one or more instances within a single study and single series by DICOM attributes.\r\n\r\n> Please see the [Conformance.md](https://github.com/microsoft/dicom-server/blob/main/docs/users/Conformance.md) file for supported DICOM attributes.\r\n\r\n_Details:_\r\n* Path: ../studies/{{study}}/series/{{series}}instances?SOPInstanceUID={{instance}}\r\n* Method: GET\r\n* Headers:\r\n * `Accept: application/dicom+json`" - }, - "response": [] - }, - { - "name": "Delete-a-specific-instance-within-a-study -and-series", - "request": { - "method": "DELETE", - "header": [], - "url": { - "raw": "{{optionallyPartitionedUrl}}/studies/{{study1}}/series/{{series1}}/instances/{{instance1}}", - "host": [ - "{{optionallyPartitionedUrl}}" - ], - "path": [ - "studies", - "{{study1}}", - "series", - "{{series1}}", - "instances", - "{{instance1}}" - ] - }, - "description": "This request deletes a single instance within a single study and single series.\r\n\r\n> Delete is not part of the DICOM standard, but has been added for convenience.\r\n\r\n_Details:_\r\n* Path: ../studies/{{study}}/series/{{series}}/instances/{{instance}}\r\n* Method: DELETE\r\n* Headers: No special headers needed" - }, - "response": [] - }, - { - "name": "Delete-a-specific-series-within-a-study", - "request": { - "method": "DELETE", - "header": [], - "url": { - "raw": "{{optionallyPartitionedUrl}}/studies/{{study1}}/series/{{series1}}", - "host": [ - "{{optionallyPartitionedUrl}}" - ], - "path": [ - "studies", - "{{study1}}", - "series", - "{{series1}}" - ] - }, - "description": "This request deletes a single series (and all child instances) within a single study.\r\n\r\n> Delete is not part of the DICOM standard, but has been added for convenience.\r\n\r\n_Details:_\r\n* Path: ../studies/{{study}}/series/{{series}}\r\n* Method: DELETE\r\n* Headers: No special headers needed" - }, - "response": [] - }, - { - "name": "Delete-a-specific-study", - "request": { - "method": "DELETE", - "header": [], - "url": { - "raw": "{{optionallyPartitionedUrl}}/studies/{{study1}}", - "host": [ - "{{optionallyPartitionedUrl}}" - ], - "path": [ - "studies", - "{{study1}}" - ] - }, - "description": "This request deletes a single study (and all child series and instances).\r\n\r\n> Delete is not part of the DICOM standard, but has been added for convenience.\r\n\r\n_Details:_\r\n* Path: ../studies/{{study}}\r\n* Method: DELETE\r\n* Headers: No special headers needed" - }, - "response": [] - }, - { - "name": "Create-extended-query-tag", - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "id": "894f58aa-f3c1-467f-ad9d-dd580349ba99" - } - } - ], - "protocolProfileBehavior": { - "disabledSystemHeaders": { - "accept": true, - "accept-encoding": true, - "connection": true, - "content-type": true, - "user-agent": true - } - }, - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "[\r\n {\r\n \"Path\": \"{{extendedQueryTag}}\",\r\n \"VR\": \"LO\",\r\n \"Level\": \"Series\"\r\n }\r\n]", - "options": { - "raw": { - "language": "text" - } - } - }, - "url": { - "raw": "{{baseUrl}}/extendedquerytags", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "extendedquerytags" - ] - }, - "description": "For the body of the request, select the red-triangle.dcm file located in the GitHub repo at ../docs/dcms. Ensure you attach the file as `binary`.\r\n\r\n> NOTE: This is a non-standard API that allows the upload of a single DICOM file without the need to configure the POST for multipart/related. It allows the use of Postman to upload files to the DICOMweb service.\r\n\r\nThe following is required to upload a single DICOM file.\r\n\r\n* Path: ../studies\r\n* Method: POST\r\n* Headers:\r\n * `Accept: application/dicom+json`\r\n * `Content-Type: application/dicom`\r\n* Body:\r\n * Contains the DICOM file as a bytes.\r\n\r\n> This API is currently not implemented\r\n" - }, - "response": [] - }, - { - "name": "List-extended-query-tags", - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "id": "1374f494-65ac-46c4-890f-2f8632988a1c" - } - } - ], - "protocolProfileBehavior": { - "disabledSystemHeaders": { - "accept": true, - "accept-encoding": true, - "connection": true, - "content-type": true, - "user-agent": true - }, - "disableBodyPruning": true - }, - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "raw", - "raw": "", - "options": { - "raw": { - "language": "text" - } - } - }, - "url": { - "raw": "{{baseUrl}}/extendedquerytags", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "extendedquerytags" - ] - }, - "description": "For the body of the request, select the red-triangle.dcm file located in the GitHub repo at ../docs/dcms. Ensure you attach the file as `binary`.\r\n\r\n> NOTE: This is a non-standard API that allows the upload of a single DICOM file without the need to configure the POST for multipart/related. It allows the use of Postman to upload files to the DICOMweb service.\r\n\r\nThe following is required to upload a single DICOM file.\r\n\r\n* Path: ../studies\r\n* Method: POST\r\n* Headers:\r\n * `Accept: application/dicom+json`\r\n * `Content-Type: application/dicom`\r\n* Body:\r\n * Contains the DICOM file as a bytes.\r\n\r\n> This API is currently not implemented\r\n" - }, - "response": [] - }, - { - "name": "Get-extended-query-tag", - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "id": "e0d29d31-6a8c-40c8-8232-a0e3f5b76de3" - } - } - ], - "protocolProfileBehavior": { - "disabledSystemHeaders": { - "accept": true, - "accept-encoding": true, - "connection": true, - "content-type": true, - "user-agent": true - }, - "disableBodyPruning": true - }, - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "raw", - "raw": "", - "options": { - "raw": { - "language": "text" - } - } - }, - "url": { - "raw": "{{baseUrl}}/extendedquerytags/{{extendedQueryTag}}", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "extendedquerytags", - "{{extendedQueryTag}}" - ] - }, - "description": "For the body of the request, select the red-triangle.dcm file located in the GitHub repo at ../docs/dcms. Ensure you attach the file as `binary`.\r\n\r\n> NOTE: This is a non-standard API that allows the upload of a single DICOM file without the need to configure the POST for multipart/related. It allows the use of Postman to upload files to the DICOMweb service.\r\n\r\nThe following is required to upload a single DICOM file.\r\n\r\n* Path: ../studies\r\n* Method: POST\r\n* Headers:\r\n * `Accept: application/dicom+json`\r\n * `Content-Type: application/dicom`\r\n* Body:\r\n * Contains the DICOM file as a bytes.\r\n\r\n> This API is currently not implemented\r\n" - }, - "response": [] - }, - { - "name": "Delete-extended-query-tag", - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "id": "d860591e-be7f-41bd-8e2c-d1f2fe9be1de" - } - } - ], - "protocolProfileBehavior": { - "disabledSystemHeaders": { - "accept": true, - "accept-encoding": true, - "connection": true, - "content-type": true, - "user-agent": true - } - }, - "request": { - "method": "DELETE", - "header": [], - "body": { - "mode": "raw", - "raw": "", - "options": { - "raw": { - "language": "text" - } - } - }, - "url": { - "raw": "{{baseUrl}}/extendedquerytags/{{extendedQueryTag}}", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "extendedquerytags", - "{{extendedQueryTag}}" - ] - }, - "description": "For the body of the request, select the red-triangle.dcm file located in the GitHub repo at ../docs/dcms. Ensure you attach the file as `binary`.\r\n\r\n> NOTE: This is a non-standard API that allows the upload of a single DICOM file without the need to configure the POST for multipart/related. It allows the use of Postman to upload files to the DICOMweb service.\r\n\r\nThe following is required to upload a single DICOM file.\r\n\r\n* Path: ../studies\r\n* Method: POST\r\n* Headers:\r\n * `Accept: application/dicom+json`\r\n * `Content-Type: application/dicom`\r\n* Body:\r\n * Contains the DICOM file as a bytes.\r\n\r\n> This API is currently not implemented\r\n" - }, - "response": [] - }, - { - "name": "Update-extended-query-tag", - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "id": "11e63a84-6cff-40a0-b9f0-ff0ecb9fffeb" - } - } - ], - "protocolProfileBehavior": { - "disabledSystemHeaders": { - "accept": true, - "accept-encoding": true, - "connection": true, - "content-type": true, - "user-agent": true - } - }, - "request": { - "method": "PATCH", - "header": [ - { - "key": "Content-Type", - "value": "application/json", - "type": "default" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"QueryStatus\": \"Disabled\"\r\n}", - "options": { - "raw": { - "language": "text" - } - } - }, - "url": { - "raw": "{{baseUrl}}/extendedquerytags/{{extendedQueryTag}}", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "extendedquerytags", - "{{extendedQueryTag}}" - ] - }, - "description": "For the body of the request, select the red-triangle.dcm file located in the GitHub repo at ../docs/dcms. Ensure you attach the file as `binary`.\r\n\r\n> NOTE: This is a non-standard API that allows the upload of a single DICOM file without the need to configure the POST for multipart/related. It allows the use of Postman to upload files to the DICOMweb service.\r\n\r\nThe following is required to upload a single DICOM file.\r\n\r\n* Path: ../studies\r\n* Method: POST\r\n* Headers:\r\n * `Accept: application/dicom+json`\r\n * `Content-Type: application/dicom`\r\n* Body:\r\n * Contains the DICOM file as a bytes.\r\n\r\n> This API is currently not implemented\r\n" - }, - "response": [] - }, - { - "name": "List-extended-query-tag-errors", - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "id": "f9a379f4-a7f5-4909-ba65-e81238253086" - } - } - ], - "protocolProfileBehavior": { - "disabledSystemHeaders": { - "accept": true, - "accept-encoding": true, - "connection": true, - "content-type": true, - "user-agent": true - }, - "disableBodyPruning": true - }, - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "raw", - "raw": "", - "options": { - "raw": { - "language": "text" - } - } - }, - "url": { - "raw": "{{baseUrl}}/extendedquerytags/{{extendedQueryTag}}/errors", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "extendedquerytags", - "{{extendedQueryTag}}", - "errors" - ] - }, - "description": "For the body of the request, select the red-triangle.dcm file located in the GitHub repo at ../docs/dcms. Ensure you attach the file as `binary`.\r\n\r\n> NOTE: This is a non-standard API that allows the upload of a single DICOM file without the need to configure the POST for multipart/related. It allows the use of Postman to upload files to the DICOMweb service.\r\n\r\nThe following is required to upload a single DICOM file.\r\n\r\n* Path: ../studies\r\n* Method: POST\r\n* Headers:\r\n * `Accept: application/dicom+json`\r\n * `Content-Type: application/dicom`\r\n* Body:\r\n * Contains the DICOM file as a bytes.\r\n\r\n> This API is currently not implemented\r\n" - }, - "response": [] - }, - { - "name": "Bulk-update-study", - "protocolProfileBehavior": { - "disabledSystemHeaders": {} - }, - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\r\n \"studyInstanceUids\": [\"{{study1}}\"],\r\n \"changeDataset\": { \r\n \"00100010\": { \r\n \"vr\": \"PN\", \r\n \"Value\": \r\n [\r\n { \r\n \"Alphabetic\": \"New Patient Name\" \r\n }\r\n ] \r\n } \r\n }\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{baseUrl}}/studies/$bulkupdate", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "studies", - "$bulkupdate" - ] - } - }, - "description": "This request updates a multiple studies.\r\n\r\n> Bulk update is not part of the DICOM standard, but has been added for convenience.\r\n\r\n_Details:_\r\n* Path: ../studies/$bulkupdate\r\n* Method: Bulk Update\r\n* Headers: No special headers needed", - "response": [] - }, - { - "name": "Get-operation", - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "id": "87f53592-f746-4a70-9c3d-2741c3978142" - } - } - ], - "protocolProfileBehavior": { - "disabledSystemHeaders": { - "accept": true, - "accept-encoding": true, - "connection": true, - "content-type": true, - "user-agent": true - }, - "disableBodyPruning": true - }, - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "raw", - "raw": "", - "options": { - "raw": { - "language": "text" - } - } - }, - "url": { - "raw": "{{baseUrl}}/operations/{{operationId}}", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "operations", - "{{operationId}}" - ] - }, - "description": "For the body of the request, select the red-triangle.dcm file located in the GitHub repo at ../docs/dcms. Ensure you attach the file as `binary`.\r\n\r\n> NOTE: This is a non-standard API that allows the upload of a single DICOM file without the need to configure the POST for multipart/related. It allows the use of Postman to upload files to the DICOMweb service.\r\n\r\nThe following is required to upload a single DICOM file.\r\n\r\n* Path: ../studies\r\n* Method: POST\r\n* Headers:\r\n * `Accept: application/dicom+json`\r\n * `Content-Type: application/dicom`\r\n* Body:\r\n * Contains the DICOM file as a bytes.\r\n\r\n> This API is currently not implemented\r\n" - }, - "response": [] - }, - { - "name": "Get-all-partitions", - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{baseUrl}}/partitions", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "partitions" - ] - } - }, - "response": [] - }, - { - "name": "Create-workitem", - "protocolProfileBehavior": { - "disabledSystemHeaders": { - "accept": true, - "accept-encoding": true, - "connection": true, - "content-type": true, - "user-agent": true - } - }, - "request": { - "method": "POST", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/dicom+json" - }, - { - "key": "Content-Type", - "type": "text", - "value": "application/dicom+json" - } - ], - "body": { - "mode": "raw", - "raw": "[\r\n {\r\n \"00081080\": {\r\n \"vr\": \"LO\",\r\n \"Value\": [\r\n \"Admitting Diagnoses Description\"\r\n ]\r\n },\r\n \"00081084\": {\r\n \"vr\": \"SQ\",\r\n \"Value\": [\r\n {\r\n \"00080104\": {\r\n \"vr\": \"LO\",\r\n \"Value\": [\r\n \"Code Meaning\"\r\n ]\r\n }\r\n }\r\n ]\r\n },\r\n \"00081195\": {\r\n \"vr\": \"UI\"\r\n },\r\n \"00100010\": {\r\n \"vr\": \"PN\",\r\n \"Value\": [\r\n {\r\n \"Alphabetic\": \"Last^First\"\r\n }\r\n ]\r\n },\r\n \"00100020\": {\r\n \"vr\": \"LO\",\r\n \"Value\": [\r\n \"11111\"\r\n ]\r\n },\r\n \"00100021\": {\r\n \"vr\": \"LO\"\r\n },\r\n \"00100024\": {\r\n \"vr\": \"SQ\"\r\n },\r\n \"00100030\": {\r\n \"vr\": \"DA\",\r\n \"Value\": [\r\n \"20000101\"\r\n ]\r\n },\r\n \"00100040\": {\r\n \"vr\": \"CS\",\r\n \"Value\": [\r\n \"F\"\r\n ]\r\n },\r\n \"00101002\": {\r\n \"vr\": \"SQ\"\r\n },\r\n \"00104000\": {\r\n \"vr\": \"LT\",\r\n \"Value\": [\r\n \"Patient Comments\"\r\n ]\r\n },\r\n \"00321033\": {\r\n \"vr\": \"LO\",\r\n \"Value\": [\r\n \"123\"\r\n ]\r\n },\r\n \"00380010\": {\r\n \"vr\": \"LO\",\r\n \"Value\": [\r\n \"11111\"\r\n ]\r\n },\r\n \"00380014\": {\r\n \"vr\": \"SQ\",\r\n \"Value\": [\r\n { }\r\n ]\r\n },\r\n \"00400400\": {\r\n \"vr\": \"LT\"\r\n },\r\n \"00401001\": {\r\n \"vr\": \"SH\",\r\n \"Value\": [\r\n \"123\"\r\n ]\r\n },\r\n \"00404005\": {\r\n \"vr\": \"DT\",\r\n \"Value\": [\r\n \"20211202115531.193\"\r\n ]\r\n },\r\n \"00404010\": {\r\n \"vr\": \"DT\",\r\n \"Value\": [\r\n \"20211202115531.193\"\r\n ]\r\n },\r\n \"00404018\": {\r\n \"vr\": \"SQ\",\r\n \"Value\": [\r\n {\r\n \"00080100\": {\r\n \"vr\": \"SH\",\r\n \"Value\": [\r\n \"ABC123\"\r\n ]\r\n },\r\n \"00080102\": {\r\n \"vr\": \"SH\",\r\n \"Value\": [\r\n \"123ABC\"\r\n ]\r\n },\r\n \"00080103\": {\r\n \"vr\": \"SH\",\r\n \"Value\": [\r\n \"1.0\"\r\n ]\r\n },\r\n \"00080104\": {\r\n \"vr\": \"LO\",\r\n \"Value\": [\r\n \"Requested procedure\"\r\n ]\r\n }\r\n }\r\n ]\r\n },\r\n \"00404021\": {\r\n \"vr\": \"SQ\"\r\n },\r\n \"00404025\": {\r\n \"vr\": \"SQ\",\r\n \"Value\": [\r\n {\r\n \"00080100\": {\r\n \"vr\": \"SH\",\r\n \"Value\": [\r\n \"ABC_123\"\r\n ]\r\n },\r\n \"00080102\": {\r\n \"vr\": \"SH\",\r\n \"Value\": [\r\n \"123ABC\"\r\n ]\r\n },\r\n \"00080104\": {\r\n \"vr\": \"LO\",\r\n \"Value\": [\r\n \"Station name\"\r\n ]\r\n }\r\n },\r\n {\r\n \"00080100\": {\r\n \"vr\": \"SH\",\r\n \"Value\": [\r\n \"DEF_456\"\r\n ]\r\n },\r\n \"00080102\": {\r\n \"vr\": \"SH\",\r\n \"Value\": [\r\n \"123ABC\"\r\n ]\r\n },\r\n \"00080104\": {\r\n \"vr\": \"LO\",\r\n \"Value\": [\r\n \"Station name\"\r\n ]\r\n }\r\n }\r\n ]\r\n },\r\n \"00404026\": {\r\n \"vr\": \"SQ\",\r\n \"Value\": [\r\n {\r\n \"00080100\": {\r\n \"vr\": \"SH\",\r\n \"Value\": [\r\n \"ABC\"\r\n ]\r\n },\r\n \"00080102\": {\r\n \"vr\": \"SH\",\r\n \"Value\": [\r\n \"AAA\"\r\n ]\r\n },\r\n \"00080104\": {\r\n \"vr\": \"LO\",\r\n \"Value\": [\r\n \"Station class\"\r\n ]\r\n }\r\n },\r\n {\r\n \"00080100\": {\r\n \"vr\": \"SH\",\r\n \"Value\": [\r\n \"ABC\"\r\n ]\r\n },\r\n \"00080102\": {\r\n \"vr\": \"SH\",\r\n \"Value\": [\r\n \"AAA\"\r\n ]\r\n },\r\n \"00080104\": {\r\n \"vr\": \"LO\",\r\n \"Value\": [\r\n \"Station class\"\r\n ]\r\n }\r\n }\r\n ]\r\n },\r\n \"00404027\": {\r\n \"vr\": \"SQ\",\r\n \"Value\": [\r\n {\r\n \"00080100\": {\r\n \"vr\": \"SH\",\r\n \"Value\": [\r\n \"ABC_1\"\r\n ]\r\n },\r\n \"00080102\": {\r\n \"vr\": \"SH\",\r\n \"Value\": [\r\n \"DEF\"\r\n ]\r\n },\r\n \"00080104\": {\r\n \"vr\": \"LO\",\r\n \"Value\": [\r\n \"Geographic location\"\r\n ]\r\n }\r\n },\r\n {\r\n \"00080100\": {\r\n \"vr\": \"SH\",\r\n \"Value\": [\r\n \"ABC_2\"\r\n ]\r\n },\r\n \"00080102\": {\r\n \"vr\": \"SH\",\r\n \"Value\": [\r\n \"DEF\"\r\n ]\r\n },\r\n \"00080104\": {\r\n \"vr\": \"LO\",\r\n \"Value\": [\r\n \"Geographic location\"\r\n ]\r\n }\r\n }\r\n ]\r\n },\r\n \"00404041\": {\r\n \"vr\": \"CS\",\r\n \"Value\": [\r\n \"READY\"\r\n ]\r\n },\r\n \"0040A370\": {\r\n \"vr\": \"SQ\",\r\n \"Value\": [\r\n {\r\n \"00080050\": {\r\n \"vr\": \"SH\",\r\n \"Value\": [\r\n \"1234567\"\r\n ]\r\n },\r\n \"00080051\": {\r\n \"vr\": \"SQ\"\r\n },\r\n \"00080090\": {\r\n \"vr\": \"PN\",\r\n \"Value\": [\r\n {\r\n \"Alphabetic\": \"Last^First^^Dr\"\r\n }\r\n ]\r\n },\r\n \"0020000D\": {\r\n \"vr\": \"UI\",\r\n \"Value\": [\r\n \"2.25.00000000000000000000000000000000\"\r\n ]\r\n },\r\n \"00321060\": {\r\n \"vr\": \"LO\",\r\n \"Value\": [\r\n \"Requested procedure description\"\r\n ]\r\n },\r\n \"00321064\": {\r\n \"vr\": \"SQ\",\r\n \"Value\": [\r\n {\r\n \"00080100\": {\r\n \"vr\": \"SH\",\r\n \"Value\": [\r\n \"GHI123\"\r\n ]\r\n },\r\n \"00080102\": {\r\n \"vr\": \"SH\",\r\n \"Value\": [\r\n \"789JKL\"\r\n ]\r\n },\r\n \"00080104\": {\r\n \"vr\": \"LO\",\r\n \"Value\": [\r\n \"Requested procedure\"\r\n ]\r\n }\r\n }\r\n ]\r\n },\r\n \"00400026\": {\r\n \"vr\": \"SQ\"\r\n },\r\n \"00400027\": {\r\n \"vr\": \"SQ\"\r\n },\r\n \"00401001\": {\r\n \"vr\": \"SH\",\r\n \"Value\": [\r\n \"123\"\r\n ]\r\n },\r\n \"00401400\": {\r\n \"vr\": \"LT\",\r\n \"Value\": [\r\n \"Requested Cataract surgery procedure comments\"\r\n ]\r\n }\r\n }\r\n ]\r\n },\r\n \"00741000\": {\r\n \"vr\": \"CS\",\r\n \"Value\": [\r\n \"SCHEDULED\"\r\n ]\r\n },\r\n \"00741002\": {\r\n \"vr\": \"SQ\"\r\n },\r\n \"00741200\": {\r\n \"vr\": \"CS\",\r\n \"Value\": [\r\n \"MEDIUM\"\r\n ]\r\n },\r\n \"00741202\": {\r\n \"vr\": \"LO\",\r\n \"Value\": [\r\n \"WORKLIST\"\r\n ]\r\n },\r\n \"00741204\": {\r\n \"vr\": \"LO\",\r\n \"Value\": [\r\n \"Scheduled procedure step description\"\r\n ]\r\n },\r\n \"00741210\": {\r\n \"vr\": \"SQ\"\r\n },\r\n \"00741216\": {\r\n \"vr\": \"SQ\"\r\n },\r\n \"0040e020\": {\r\n \"vr\": \"CS\",\r\n \"Value\": [\r\n \"Type of Instances\"\r\n ]\r\n },\r\n \"00081199\": {\r\n \"vr\": \"SQ\",\r\n \"Value\": [\r\n {\r\n \"00081150\": {\r\n \"vr\": \"UI\",\r\n \"Value\": [\r\n \"2.5.0000000\"\r\n ]\r\n },\r\n \"00081155\": {\r\n \"vr\": \"UI\",\r\n \"Value\": [\r\n \"2.5.1000000\"\r\n ]\r\n }\r\n }\r\n ]\r\n }\r\n }\r\n]", - "options": { - "raw": { - "language": "text" - } - } - }, - "url": { - "raw": "{{optionallyPartitionedUrl}}/workitems?{{workitem1}}", - "host": [ - "{{optionallyPartitionedUrl}}" - ], - "path": [ - "workitems" - ], - "query": [ - { - "key": "{{workitem1}}", - "value": null - } - ] - }, - "description": "The following is required.\n\n* Method: POST\n* Headers:\n * `Accept: application/dicom+json`\n * `Content-Type: application/dicom+json`\n* Body:\n * Contains the workitem dataset in dicom+json format." - }, - "response": [] - }, - { - "name": "Search-for-workitems", - "protocolProfileBehavior": { - "disabledSystemHeaders": { - "accept": true, - "accept-encoding": true, - "connection": true, - "content-type": true, - "user-agent": true - }, - "disableBodyPruning": true - }, - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/dicom+json" - }, - { - "key": "Content-Type", - "type": "text", - "value": "application/dicom+json" - } - ], - "body": { - "mode": "raw", - "raw": "[\r\n {\r\n \"00081080\": {\r\n \"vr\": \"LO\"\r\n },\r\n \"00081084\": {\r\n \"vr\": \"SQ\",\r\n \"Value\": [\r\n { }\r\n ]\r\n },\r\n \"00081195\": {\r\n \"vr\": \"UI\"\r\n },\r\n \"00100010\": {\r\n \"vr\": \"PN\",\r\n \"Value\": [\r\n {\r\n \"Alphabetic\": \"Last^First\"\r\n }\r\n ]\r\n },\r\n \"00100020\": {\r\n \"vr\": \"LO\",\r\n \"Value\": [\r\n \"11111\"\r\n ]\r\n },\r\n \"00100030\": {\r\n \"vr\": \"DA\",\r\n \"Value\": [\r\n \"20000101\"\r\n ]\r\n },\r\n \"00100040\": {\r\n \"vr\": \"CS\",\r\n \"Value\": [\r\n \"F\"\r\n ]\r\n },\r\n \"00101002\": {\r\n \"vr\": \"SQ\"\r\n },\r\n \"00104000\": {\r\n \"vr\": \"LT\"\r\n },\r\n \"00321033\": {\r\n \"vr\": \"LO\",\r\n \"Value\": [\r\n \"123\"\r\n ]\r\n },\r\n \"00380010\": {\r\n \"vr\": \"LO\",\r\n \"Value\": [\r\n \"11111\"\r\n ]\r\n },\r\n \"00380014\": {\r\n \"vr\": \"SQ\"\r\n },\r\n \"00400400\": {\r\n \"vr\": \"LT\"\r\n },\r\n \"00401001\": {\r\n \"vr\": \"SH\",\r\n \"Value\": [\r\n \"123\"\r\n ]\r\n },\r\n \"00404005\": {\r\n \"vr\": \"DT\",\r\n \"Value\": [\r\n \"20211202115531.193\"\r\n ]\r\n },\r\n \"00404010\": {\r\n \"vr\": \"DT\",\r\n \"Value\": [\r\n \"20211202115531.193\"\r\n ]\r\n },\r\n \"00404018\": {\r\n \"vr\": \"SQ\",\r\n \"Value\": [\r\n {\r\n \"00080100\": {\r\n \"vr\": \"SH\",\r\n \"Value\": [\r\n \"ABC123\"\r\n ]\r\n },\r\n \"00080102\": {\r\n \"vr\": \"SH\",\r\n \"Value\": [\r\n \"123ABC\"\r\n ]\r\n },\r\n \"00080103\": {\r\n \"vr\": \"SH\",\r\n \"Value\": [\r\n \"1.0\"\r\n ]\r\n },\r\n \"00080104\": {\r\n \"vr\": \"LO\",\r\n \"Value\": [\r\n \"Requested procedure\"\r\n ]\r\n }\r\n }\r\n ]\r\n },\r\n \"00404021\": {\r\n \"vr\": \"SQ\"\r\n },\r\n \"00404025\": {\r\n \"vr\": \"SQ\",\r\n \"Value\": [\r\n {\r\n \"00080100\": {\r\n \"vr\": \"SH\",\r\n \"Value\": [\r\n \"ABC_123\"\r\n ]\r\n },\r\n \"00080102\": {\r\n \"vr\": \"SH\",\r\n \"Value\": [\r\n \"123ABC\"\r\n ]\r\n },\r\n \"00080104\": {\r\n \"vr\": \"LO\",\r\n \"Value\": [\r\n \"Station name\"\r\n ]\r\n }\r\n },\r\n {\r\n \"00080100\": {\r\n \"vr\": \"SH\",\r\n \"Value\": [\r\n \"DEF_456\"\r\n ]\r\n },\r\n \"00080102\": {\r\n \"vr\": \"SH\",\r\n \"Value\": [\r\n \"123ABC\"\r\n ]\r\n },\r\n \"00080104\": {\r\n \"vr\": \"LO\",\r\n \"Value\": [\r\n \"Station name\"\r\n ]\r\n }\r\n }\r\n ]\r\n },\r\n \"00404026\": {\r\n \"vr\": \"SQ\",\r\n \"Value\": [\r\n {\r\n \"00080100\": {\r\n \"vr\": \"SH\",\r\n \"Value\": [\r\n \"ABC\"\r\n ]\r\n },\r\n \"00080102\": {\r\n \"vr\": \"SH\",\r\n \"Value\": [\r\n \"AAA\"\r\n ]\r\n },\r\n \"00080104\": {\r\n \"vr\": \"LO\",\r\n \"Value\": [\r\n \"Station class\"\r\n ]\r\n }\r\n },\r\n {\r\n \"00080100\": {\r\n \"vr\": \"SH\",\r\n \"Value\": [\r\n \"ABC\"\r\n ]\r\n },\r\n \"00080102\": {\r\n \"vr\": \"SH\",\r\n \"Value\": [\r\n \"AAA\"\r\n ]\r\n },\r\n \"00080104\": {\r\n \"vr\": \"LO\",\r\n \"Value\": [\r\n \"Station class\"\r\n ]\r\n }\r\n }\r\n ]\r\n },\r\n \"00404027\": {\r\n \"vr\": \"SQ\",\r\n \"Value\": [\r\n {\r\n \"00080100\": {\r\n \"vr\": \"SH\",\r\n \"Value\": [\r\n \"ABC_1\"\r\n ]\r\n },\r\n \"00080102\": {\r\n \"vr\": \"SH\",\r\n \"Value\": [\r\n \"DEF\"\r\n ]\r\n },\r\n \"00080104\": {\r\n \"vr\": \"LO\",\r\n \"Value\": [\r\n \"Geographic location\"\r\n ]\r\n }\r\n },\r\n {\r\n \"00080100\": {\r\n \"vr\": \"SH\",\r\n \"Value\": [\r\n \"ABC_2\"\r\n ]\r\n },\r\n \"00080102\": {\r\n \"vr\": \"SH\",\r\n \"Value\": [\r\n \"DEF\"\r\n ]\r\n },\r\n \"00080104\": {\r\n \"vr\": \"LO\",\r\n \"Value\": [\r\n \"Geographic location\"\r\n ]\r\n }\r\n }\r\n ]\r\n },\r\n \"00404034\": {\r\n \"vr\": \"SQ\",\r\n \"Value\": [\r\n {\r\n \"00404037\": {\r\n \"vr\": \"PN\",\r\n \"Value\": [\r\n {\r\n \"Alphabetic\": \"Lastname^Firstname^^Dr\"\r\n }\r\n ]\r\n }\r\n }\r\n ]\r\n },\r\n \"00404041\": {\r\n \"vr\": \"CS\",\r\n \"Value\": [\r\n \"READY\"\r\n ]\r\n },\r\n \"0040A370\": {\r\n \"vr\": \"SQ\",\r\n \"Value\": [\r\n {\r\n \"00080050\": {\r\n \"vr\": \"SH\",\r\n \"Value\": [\r\n \"1234567\"\r\n ]\r\n },\r\n \"00080051\": {\r\n \"vr\": \"SQ\"\r\n },\r\n \"00080090\": {\r\n \"vr\": \"PN\",\r\n \"Value\": [\r\n {\r\n \"Alphabetic\": \"Last^First^^Dr\"\r\n }\r\n ]\r\n },\r\n \"0020000D\": {\r\n \"vr\": \"UI\",\r\n \"Value\": [\r\n \"2.25.00000000000000000000000000000000\"\r\n ]\r\n },\r\n \"00321060\": {\r\n \"vr\": \"LO\",\r\n \"Value\": [\r\n \"Requested procedure description\"\r\n ]\r\n },\r\n \"00321064\": {\r\n \"vr\": \"SQ\",\r\n \"Value\": [\r\n {\r\n \"00080100\": {\r\n \"vr\": \"SH\",\r\n \"Value\": [\r\n \"GHI123\"\r\n ]\r\n },\r\n \"00080102\": {\r\n \"vr\": \"SH\",\r\n \"Value\": [\r\n \"789JKL\"\r\n ]\r\n },\r\n \"00080104\": {\r\n \"vr\": \"LO\",\r\n \"Value\": [\r\n \"Requested procedure\"\r\n ]\r\n }\r\n }\r\n ]\r\n },\r\n \"00400026\": {\r\n \"vr\": \"SQ\"\r\n },\r\n \"00400027\": {\r\n \"vr\": \"SQ\"\r\n },\r\n \"00401001\": {\r\n \"vr\": \"SH\",\r\n \"Value\": [\r\n \"123\"\r\n ]\r\n },\r\n \"00401400\": {\r\n \"vr\": \"LT\",\r\n \"Value\": [\r\n \"Requested Cataract surgery procedure comments\"\r\n ]\r\n }\r\n }\r\n ]\r\n },\r\n \"00741000\": {\r\n \"vr\": \"CS\",\r\n \"Value\": [\r\n \"SCHEDULED\"\r\n ]\r\n },\r\n \"00741002\": {\r\n \"vr\": \"SQ\"\r\n },\r\n \"00741200\": {\r\n \"vr\": \"CS\",\r\n \"Value\": [\r\n \"MEDIUM\"\r\n ]\r\n },\r\n \"00741202\": {\r\n \"vr\": \"LO\",\r\n \"Value\": [\r\n \"WORKLIST\"\r\n ]\r\n },\r\n \"00741204\": {\r\n \"vr\": \"LO\",\r\n \"Value\": [\r\n \"Scheduled procedure step description\"\r\n ]\r\n },\r\n \"00741210\": {\r\n \"vr\": \"SQ\"\r\n },\r\n \"00741216\": {\r\n \"vr\": \"SQ\"\r\n }\r\n }\r\n]", - "options": { - "raw": { - "language": "text" - } - } - }, - "url": { - "raw": "{{optionallyPartitionedUrl}}/workitems?PatientName=Last^First", - "host": [ - "{{optionallyPartitionedUrl}}" - ], - "path": [ - "workitems" - ], - "query": [ - { - "key": "PatientName", - "value": "Last^First" - } - ] - }, - "description": "The following is required.\n\n* Method: GET\n* Headers:\n * `Accept: application/dicom+json`\n * `Content-Type: application/dicom+json`\n* Query String:\n * `FuzzyMatching=true` OR `FuzzyMatching=false`\n * Use `name=value` format for filter fields\n * Use `IncludeField` with comma separated values\n * Use `Limit` to retrieve a specific number of work-items (i.e., to indicate page-size)\n * Use `Offset` to indicate page-index" - }, - "response": [] - }, - { - "name": "Request-cancellation-workitem", - "protocolProfileBehavior": { - "disabledSystemHeaders": { - "accept": true, - "accept-encoding": true, - "connection": true, - "content-type": true, - "user-agent": true - } - }, - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "type": "text", - "value": "application/dicom+json" - } - ], - "body": { - "mode": "raw", - "raw": "[\r\n {\r\n \"00741238\": {\r\n \"vr\": \"LT\",\r\n \"Value\": [\r\n \"This Workitem should be cancelled.\"\r\n ]\r\n },\r\n \"0074100A\": {\r\n \"vr\": \"UR\",\r\n \"Value\": [\r\n \"https://microsoft.com\"\r\n ]\r\n },\r\n \"0074100C\": {\r\n \"vr\": \"LO\",\r\n \"Value\": [\r\n \"Microsoft\"\r\n ]\r\n },\r\n \"0074100E\": {\r\n \"vr\": \"SQ\",\r\n \"Value\": [\r\n {\r\n \"00080100\": {\r\n \"vr\": \"SH\",\r\n \"Value\": [\r\n \"ABC123\"\r\n ]\r\n },\r\n \"00080102\": {\r\n \"vr\": \"SH\",\r\n \"Value\": [\r\n \"123ABC\"\r\n ]\r\n },\r\n \"00080104\": {\r\n \"vr\": \"LO\",\r\n \"Value\": [\r\n \"Requested procedure\"\r\n ]\r\n }\r\n }\r\n ]\r\n }\r\n }\r\n]", - "options": { - "raw": { - "language": "text" - } - } - }, - "url": { - "raw": "{{optionallyPartitionedUrl}}/workitems/{{workitem1}}/cancelrequest", - "host": [ - "{{optionallyPartitionedUrl}}" - ], - "path": [ - "workitems", - "{{workitem1}}", - "cancelrequest" - ] - }, - "description": "The following is required.\n\n* Method: POST\n* Headers:\n * `Content-Type: application/dicom+json`\n* Body:\n * Contains the workitem cancel request dataset in dicom+json format." - }, - "response": [] - }, - { - "name": "Change-Workitem-State", - "protocolProfileBehavior": { - "disabledSystemHeaders": { - "accept": true, - "accept-encoding": true, - "connection": true, - "content-type": true, - "user-agent": true - } - }, - "request": { - "method": "PUT", - "header": [ - { - "key": "Content-Type", - "type": "text", - "value": "application/dicom+json" - } - ], - "body": { - "mode": "raw", - "raw": "[\r\n {\r\n \"00741000\": {\r\n \"vr\": \"CS\",\r\n \"Value\": [\r\n \"IN PROGRESS\"\r\n ]\r\n },\r\n \"00081195\": {\r\n \"vr\": \"UI\",\r\n \"Value\": [\r\n \"{{transactionUid}}\"\r\n ]\r\n }\r\n }\r\n]", - "options": { - "raw": { - "language": "text" - } - } - }, - "url": { - "raw": "{{optionallyPartitionedUrl}}/workitems/{{workitem1}}/state", - "host": [ - "{{optionallyPartitionedUrl}}" - ], - "path": [ - "workitems", - "{{workitem1}}", - "state" - ] - }, - "description": "The following is required.\n\n* Method: PUT\n* Headers:\n * `Content-Type: application/dicom+json`\n* Body:\n * Contains the change workitem state request dataset in dicom+json format." - }, - "response": [] - }, - { - "name": "Update-unclaimed-workitem", - "protocolProfileBehavior": { - "disabledSystemHeaders": { - "accept": true, - "accept-encoding": true, - "connection": true, - "content-type": true, - "user-agent": true - } - }, - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "type": "text", - "value": "application/dicom+json" - } - ], - "body": { - "mode": "raw", - "raw": "[\r\n {\r\n \"00741202\": {\r\n \"vr\": \"LO\",\r\n \"Value\": [\r\n \"New Worklist Label\"\r\n ]\r\n },\r\n \"0040e020\": {\r\n \"vr\": \"CS\",\r\n \"Value\": [\r\n \"Type of Instances\"\r\n ]\r\n },\r\n \"00081199\": {\r\n \"vr\": \"SQ\",\r\n \"Value\": [\r\n {\r\n \"00081150\": {\r\n \"vr\": \"UI\",\r\n \"Value\": [\r\n \"2.5.0000000\"\r\n ]\r\n },\r\n \"00081155\": {\r\n \"vr\": \"UI\",\r\n \"Value\": [\r\n \"2.5.1000000\"\r\n ]\r\n }\r\n }\r\n ]\r\n }\r\n }\r\n]", - "options": { - "raw": { - "language": "text" - } - } - }, - "url": { - "raw": "{{optionallyPartitionedUrl}}/workitems/{{workitem1}}", - "host": [ - "{{optionallyPartitionedUrl}}" - ], - "path": [ - "workitems", - "{{workitem1}}" - ] - }, - "description": "The following is required.\n\n* Method: POST\n* Headers:\n * `Content-Type: application/dicom+json`\n* Body:\n * Contains the update workitem dataset in dicom+json format." - }, - "response": [] - }, - { - "name": "Update-claimed-workitem", - "protocolProfileBehavior": { - "disabledSystemHeaders": { - "accept": true, - "accept-encoding": true, - "connection": true, - "content-type": true, - "user-agent": true - } - }, - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "type": "text", - "value": "application/dicom+json" - } - ], - "body": { - "mode": "raw", - "raw": "[\r\n {\r\n \"00741202\": {\r\n \"vr\": \"LO\",\r\n \"Value\": [\r\n \"New Worklist Label2\"\r\n ]\r\n },\r\n \"0040e020\": {\r\n \"vr\": \"CS\",\r\n \"Value\": [\r\n \"Type of Instances\"\r\n ]\r\n },\r\n \"00081199\": {\r\n \"vr\": \"SQ\",\r\n \"Value\": [\r\n {\r\n \"00081150\": {\r\n \"vr\": \"UI\",\r\n \"Value\": [\r\n \"2.5.0000000\"\r\n ]\r\n },\r\n \"00081155\": {\r\n \"vr\": \"UI\",\r\n \"Value\": [\r\n \"2.5.1000000\"\r\n ]\r\n }\r\n }\r\n ]\r\n }\r\n }\r\n]", - "options": { - "raw": { - "language": "text" - } - } - }, - "url": { - "raw": "{{optionallyPartitionedUrl}}/workitems/{{workitem1}}?{{transactionUid}}", - "host": [ - "{{optionallyPartitionedUrl}}" - ], - "path": [ - "workitems", - "{{workitem1}}" - ], - "query": [ - { - "key": "{{transactionUid}}", - "value": null - } - ] - }, - "description": "The following is required.\n\n* Method: POST\n* Headers:\n * `Content-Type: application/dicom+json`\n* Body:\n * Contains the update workitem dataset in dicom+json format." - }, - "response": [] - }, - { - "name": "Retrieve-workitem", - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/dicom+json", - "type": "text" - } - ], - "url": { - "raw": "{{optionallyPartitionedUrl}}/workitems/{{workitem1}}", - "host": [ - "{{optionallyPartitionedUrl}}" - ], - "path": [ - "workitems", - "{{workitem1}}" - ] - }, - "description": "The following is required.\n\n* Method: GET\n* Headers:\n * `Accept: application/dicom+json`" - }, - "response": [] - } - ], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "", - "type": "string" - } - ] - }, - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "const partitionName = pm.collectionVariables.get(\"partitionName\")", - "const baseUrl = pm.collectionVariables.get(\"baseUrl\")", - "", - "if(partitionName !== \"\") {", - " pm.collectionVariables.set(\"optionallyPartitionedUrl\", baseUrl + \"/partitions/{{partitionName}}\")", - "} else {", - " pm.collectionVariables.set(\"optionallyPartitionedUrl\", baseUrl)", - "}" - ], - "id": "7a39bc47-5125-4e1f-b322-3bc04ff57e11" - } - }, - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "" - ], - "id": "fcedbb83-d444-4d89-ad61-3ce84ce37ad7" - } - } - ], - "variable": [ - { - "id": "e21c8a39-0224-4654-aa1f-446e02ad3af5", - "key": "baseUrl", - "value": "https://localhost:63838/v1" - }, - { - "id": "39d5d2ab-96cf-4daa-8209-8893c94e85f4", - "key": "study1", - "value": "1.2.826.0.1.3680043.8.498.13230779778012324449356534479549187420" - }, - { - "id": "f77cbd7b-053e-4a79-914b-1ddd79e9c19d", - "key": "series1", - "value": "1.2.826.0.1.3680043.8.498.45787841905473114233124723359129632652" - }, - { - "id": "46319575-e1d4-4f3f-bed5-1cc2fa3b784c", - "key": "instance1", - "value": "1.2.826.0.1.3680043.8.498.47359123102728459884412887463296905395" - }, - { - "id": "baf10350-9b81-496d-ab17-511b29ee9fce", - "key": "instance2", - "value": "1.2.826.0.1.3680043.8.498.12714725698140337137334606354172323212\n" - }, - { - "id": "c1070cfd-fe2f-4b8c-8df1-26c23e68b655", - "key": "extendedQueryTag", - "value": "ManufacturerModelName" - }, - { - "id": "1f242804-812f-4df9-99f0-a60e177e7cc5", - "key": "operationId", - "value": "1" - }, - { - "id": "fad4e434-a3b4-4a69-93d9-9b0d12b38d0a", - "key": "partitionName", - "value": "" - }, - { - "id": "c7bb4ae5-7964-43db-983e-a3d447b54e67", - "key": "optionallyPartitionedUrl", - "value": "" - }, - { - "id": "85568294-089e-4f15-8101-c40d0a83bd19", - "key": "workitem1", - "value": "1" - }, - { - "id": "68344164-240f-47e2-bcaa-1ac5b5b3af11", - "key": "transactionUid", - "value": "1.2.3" - } - ], - "protocolProfileBehavior": {} -} \ No newline at end of file diff --git a/docs/resources/conformance-statement.md b/docs/resources/conformance-statement.md deleted file mode 100644 index 5baa1b219d..0000000000 --- a/docs/resources/conformance-statement.md +++ /dev/null @@ -1,832 +0,0 @@ -# DICOM Conformance Statement - -> API version 2 is the latest API version and should be used. -> Go to [API version 2](v2-conformance-statement.md) for the latest conformance statement. - -The **Medical Imaging Server for DICOM** supports a subset of the DICOMweb™ Standard. Support includes: - -- [Studies Service](#studies-service) - - [Store (STOW-RS)](#store-stow-rs) - - [Retrieve (WADO-RS)](#retrieve-wado-rs) - - [Search (QIDO-RS)](#search-qido-rs) - - [Delete (Non-standard)](#delete) -- [Worklist Service (UPS Push and Pull SOPs)](#worklist-service-ups-rs) - - [Create Workitem](#create-workitem) - - [Retrieve Workitem](#retrieve-workitem) - - [Update Workitem](#update-workitem) - - [Change Workitem State](#change-workitem-state) - - [Request Cancellation](#request-cancellation) - - [Search Workitems](#search-workitems) - -Additionally, the following non-standard API(s) are supported: -- [Change Feed](../concepts/change-feed.md) -- [Extended Query Tags](../concepts/extended-query-tags.md) - -All paths below include an implicit base URL of the server, such as `https://localhost:63838` when running locally. - -The service makes use of REST Api versioning. Do note that the version of the REST API must be explicitly specified as part of the base URL as in the following example: - -`https://localhost:63838/v1/studies` - -For more information on how to specify the version when making requests, visit the [Api Versioning Documentation](../api-versioning.md). - -You can find example requests for supported transactions in the [Postman collection](../resources/Conformance-as-Postman.postman_collection.json). - -## Preamble Sanitization - -The service ignores the 128-byte File Preamble, and replaces its contents with null characters. This ensures that no files passed through the service are -vulnerable to the [malicious preamble vulnerability](https://dicom.nema.org/medical/dicom/current/output/chtml/part10/sect_7.5.html). However, this also means -that [preambles used to encode dual format content](https://www.ncbi.nlm.nih.gov/pmc/articles/PMC6489422/) such as TIFF cannot be used with the service. - -# Studies Service - -The [Studies Service](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#chapter_10) allows users to store, retrieve, and search for DICOM Studies, Series, and Instances. We have added the non-standard Delete transaction to enable a full resource lifecycle. - -## Store (STOW-RS) - -This transaction uses the POST method to Store representations of Studies, Series, and Instances contained in the request payload. - -| Method | Path | Description | -| :----- | :----------------- | :---------- | -| POST | ../studies | Store instances. | -| POST | ../studies/{study} | Store instances for a specific study. | - -Parameter `study` corresponds to the DICOM attribute StudyInstanceUID. If specified, any instance that does not belong to the provided study will be rejected with `43265` warning code. - -The following `Accept` header(s) for the response are supported: - -- `application/dicom+json` - -The following `Content-Type` header(s) are supported: - -- `multipart/related; type="application/dicom"` -- `application/dicom` - -> Note: The Server will not coerce or replace attributes that conflict with existing data. All data will be stored as provided. - -The following DICOM elements are required to be present in every DICOM file attempting to be stored: - -- StudyInstanceUID -- SeriesInstanceUID -- SOPInstanceUID -- SOPClassUID -- PatientID - -> Note: All UIDs must be between 1 and 64 characters long, and only contain alpha numeric characters or the following special characters: `.`, `-`. PatientID is validated based on its LO VR type. - -Each file stored must have a unique combination of StudyInstanceUID, SeriesInstanceUID and SopInstanceUID. The warning code `45070` will be returned if a file with the same identifiers already exists. - -> DICOM File Size Limit: there is a size limit of 2GB for a DICOM file by default. - -### Store Response Status Codes - -| Code | Description | -| :--------------------------- | :---------- | -| 200 (OK) | All the SOP instances in the request have been stored. | -| 202 (Accepted) | Some instances in the request have been stored but others have failed. | -| 204 (No Content) | No content was provided in the store transaction request. | -| 400 (Bad Request) | The request was badly formatted. For example, the provided study instance identifier did not conform the expected UID format. | -| 401 (Unauthorized) | The client is not authenticated. | -| 406 (Not Acceptable) | The specified `Accept` header is not supported. | -| 409 (Conflict) | None of the instances in the store transaction request have been stored. | -| 415 (Unsupported Media Type) | The provided `Content-Type` is not supported. | -| 503 (Service Unavailable) | The service is unavailable or busy. Please try again later. | - -### Store Response Payload - -The response payload will populate a DICOM dataset with the following elements: - -| Tag | Name | Description | -| :----------- | :-------------------- | :---------- | -| (0008, 1190) | RetrieveURL | The Retrieve URL of the study if the StudyInstanceUID was provided in the store request and at least one instance is successfully stored. | -| (0008, 1198) | FailedSOPSequence | The sequence of instances that failed to store. | -| (0008, 1199) | ReferencedSOPSequence | The sequence of stored instances. | - -Each dataset in the `FailedSOPSequence` will have the following elements (if the DICOM file attempting to be stored could be read): - -| Tag | Name | Description | -| :----------- | :----------------------- | :---------- | -| (0008, 1150) | ReferencedSOPClassUID | The SOP class unique identifier of the instance that failed to store. | -| (0008, 1155) | ReferencedSOPInstanceUID | The SOP instance unique identifier of the instance that failed to store. | -| (0008, 1197) | FailureReason | The reason code why this instance failed to store. | -| (0074, 1048) | FailedAttributesSequence | The sequence of `ErrorComment` that includes the reason for each failed attribute. | - -Each dataset in the `ReferencedSOPSequence` will have the following elements: - -| Tag | Name | Description | -| :----------- | :----------------------- | :---------- | -| (0008, 1150) | ReferencedSOPClassUID | The SOP class unique identifier of the instance that was stored. | -| (0008, 1155) | ReferencedSOPInstanceUID | The SOP instance unique identifier of the instance that was stored. | -| (0008, 1190) | RetrieveURL | The retrieve URL of this instance on the DICOM server. | - -An example response with `Accept` header `application/dicom+json`: - -```json -{ - "00081190": - { - "vr":"UR", - "Value":["http://localhost/studies/d09e8215-e1e1-4c7a-8496-b4f6641ed232"] - }, - "00081198": - { - "vr":"SQ", - "Value": - [{ - "00081150": - { - "vr":"UI","Value":["cd70f89a-05bc-4dab-b6b8-1f3d2fcafeec"] - }, - "00081155": - { - "vr":"UI", - "Value":["22c35d16-11ce-43fa-8f86-90ceed6cf4e7"] - }, - "00081197": - { - "vr":"US", - "Value":[43265] - } - }] - }, - "00081199": - { - "vr":"SQ", - "Value": - [{ - "00081150": - { - "vr":"UI", - "Value":["d246deb5-18c8-4336-a591-aeb6f8596664"] - }, - "00081155": - { - "vr":"UI", - "Value":["4a858cbb-a71f-4c01-b9b5-85f88b031365"] - }, - "00081190": - { - "vr":"UR", - "Value":["http://localhost/studies/d09e8215-e1e1-4c7a-8496-b4f6641ed232/series/8c4915f5-cc54-4e50-aa1f-9b06f6e58485/instances/4a858cbb-a71f-4c01-b9b5-85f88b031365"] - } - }] - } -} -``` - -### Store Failure Reason Codes - -| Code | Description | -| :---- | :---------- | -| 272 | The store transaction did not store the instance because of a general failure in processing the operation. | -| 43264 | The DICOM instance failed the validation. | -| 43265 | The provided instance StudyInstanceUID did not match the specified StudyInstanceUID in the store request. | -| 45070 | A DICOM instance with the same StudyInstanceUID, SeriesInstanceUID and SopInstanceUID has already been stored. If you wish to update the contents, delete this instance first. | -| 45071 | A DICOM instance is being created by another process, or the previous attempt to create has failed and the cleanup process has not had chance to clean up yet. Please delete the instance first before attempting to create again. | -| 45063 | A DICOM instance Data Set does not match SOP Class. The Studies Store Transaction (Section 10.5) observed that the Data Set did not match the constraints of the SOP Class during storage of the instance. | - -### Store Error Codes - -| Code | Description | -| :---- | :---------- | -| 100 | The provided instance attributes did not meet the validation criteria. | - -## Retrieve (WADO-RS) - -This Retrieve Transaction offers support for retrieving stored studies, series, instances and frames by reference. - -| Method | Path | Description | -| :----- | :---------------------------------------------------------------------- | :---------- | -| GET | ../studies/{study} | Retrieves all instances within a study. | -| GET | ../studies/{study}/metadata | Retrieves the metadata for all instances within a study. | -| GET | ../studies/{study}/series/{series} | Retrieves all instances within a series. | -| GET | ../studies/{study}/series/{series}/metadata | Retrieves the metadata for all instances within a series. | -| GET | ../studies/{study}/series/{series}/instances/{instance} | Retrieves a single instance. | -| GET | ../studies/{study}/series/{series}/instances/{instance}/metadata | Retrieves the metadata for a single instance. | -| GET | ../studies/{study}/series/{series}/instances/{instance}/rendered | Retrieves an instance rendered into an image format | -| GET | ../studies/{study}/series/{series}/instances/{instance}/frames/{frames} | Retrieves one or many frames from a single instance. To specify more than one frame, a comma separate each frame to return, e.g. /studies/1/series/2/instance/3/frames/4,5,6 | -| GET | ../studies/{study}/series/{series}/instances/{instance}/frames/{frame}/rendered | Retrieves a single frame rendered into an image format | - -### Retrieve instances within Study or Series - -The following `Accept` header(s) are supported for retrieving instances within a study or a series: - - -- `multipart/related; type="application/dicom"; transfer-syntax=*` -- `multipart/related; type="application/dicom";` (when transfer-syntax is not specified, 1.2.840.10008.1.2.1 is used as default) -- `multipart/related; type="application/dicom"; transfer-syntax=1.2.840.10008.1.2.1` -- `multipart/related; type="application/dicom"; transfer-syntax=1.2.840.10008.1.2.4.90` -- `*/*` (when transfer-syntax is not specified, `*` is used as default and mediaType defaults to `application/dicom`) - -### Retrieve an Instance - -The following `Accept` header(s) are supported for retrieving a specific instance: - -- `application/dicom; transfer-syntax=*` -- `multipart/related; type="application/dicom"; transfer-syntax=*` -- `application/dicom;` (when transfer-syntax is not specified, `1.2.840.10008.1.2.1` is used as default) -- `multipart/related; type="application/dicom"` (when transfer-syntax is not specified, `1.2.840.10008.1.2.1` is used as default) -- `application/dicom; transfer-syntax=1.2.840.10008.1.2.1` -- `multipart/related; type="application/dicom"; transfer-syntax=1.2.840.10008.1.2.1` -- `application/dicom; transfer-syntax=1.2.840.10008.1.2.4.90` -- `multipart/related; type="application/dicom"; transfer-syntax=1.2.840.10008.1.2.4.90` -- `*/*` (when transfer-syntax is not specified, `*` is used as default and mediaType defaults to `application/dicom`) - -### Retrieve Frames - -The following `Accept` headers are supported for retrieving frames: -- `multipart/related; type="application/octet-stream"; transfer-syntax=*` -- `multipart/related; type="application/octet-stream";` (when transfer-syntax is not specified, `1.2.840.10008.1.2.1` is used as default) -- `multipart/related; type="application/octet-stream"; transfer-syntax=1.2.840.10008.1.2.1` -- `multipart/related; type="image/jp2";` (when transfer-syntax is not specified, `1.2.840.10008.1.2.4.90` is used as default) -- `multipart/related; type="image/jp2";transfer-syntax=1.2.840.10008.1.2.4.90` -- `application/octet-stream; transfer-syntax=*` for single frame retrieval -- `*/*` (when transfer-syntax is not specified, `*` is used as default and mediaType defaults to `application/octet-stream`) - -### Retrieve Transfer Syntax - -When the requested transfer syntax is different from original file, the original file is transcoded to requested transfer syntax. The original file needs to be one of below formats for transcoding to succeed, otherwise transcoding may fail: -- 1.2.840.10008.1.2 (Little Endian Implicit) -- 1.2.840.10008.1.2.1 (Little Endian Explicit) -- 1.2.840.10008.1.2.2 (Explicit VR Big Endian) -- 1.2.840.10008.1.2.4.50 (JPEG Baseline Process 1) -- 1.2.840.10008.1.2.4.57 (JPEG Lossless) -- 1.2.840.10008.1.2.4.70 (JPEG Lossless Selection Value 1) -- 1.2.840.10008.1.2.4.90 (JPEG 2000 Lossless Only) -- 1.2.840.10008.1.2.4.91 (JPEG 2000) -- 1.2.840.10008.1.2.5 (RLE Lossless) - -An unsupported `transfer-syntax` will result in `406 Not Acceptable`. - -### Retrieve Metadata (for Study, Series, or Instance) - -The following `Accept` header(s) are supported for retrieving metadata for a study, a series, or an instance: - -- `application/dicom+json` - -Retrieving metadata will not return attributes with the following value representations: - -| VR Name | Description | -| :------ | :--------------------- | -| OB | Other Byte | -| OD | Other Double | -| OF | Other Float | -| OL | Other Long | -| OV | Other 64-Bit Very Long | -| OW | Other Word | -| UN | Unknown | - -### Retrieve Metadata Cache Validation (for Study, Series, or Instance) - -Cache validation is supported using the `ETag` mechanism. In the response of a metadata reqeuest, ETag is returned as one of the headers. This ETag can be cached and added as `If-None-Match` header in the later requests for the same metadata. Two types of responses are possible if the data exists: -- Data has not changed since the last request: HTTP 304 (Not Modified) response will be sent with no body. -- Data has changed since the last request: HTTP 200 (OK) response will be sent with updated ETag. Required data will also be returned as part of the body. - -### Retrieve Rendered Image (For Instance or Frame) -The following `Accept` header(s) are supported for retrieving a rendered image an instance or a frame: - -- `image/jpeg` -- `image/png` - -In the case that no `Accept` header is specified the service will render an `image/jpeg` by default. - -The service only supports rendering of a single frame. If rendering is requested for an instance with multiple frames then only the first frame will be rendered as an image by default. - -When specifying a particular frame to return, frame indexing starts at 1. - -The `quality` query parameter is also supported. An integer value between `1-100` inclusive (1 being worst quality, and 100 being best quality) may be passed as the value for the query paramater. This will only be used for images rendered as `jpeg`, and will be ignored for `png` render requests. If not specified will default to `100`. - - -### Retrieve Response Status Codes - -| Code | Description | -| :--------------------------- | :---------- | -| 200 (OK) | All requested data has been retrieved. | -| 304 (Not Modified) | The requested data has not modified since the last request. Content is not added to the response body in such case. Please see [Retrieve Metadata Cache Validation (for Study, Series, or Instance)](###Retrieve-Metadata-Cache-Validation-(for-Study,-Series,-or-Instance)) for more information. | -| 400 (Bad Request) | The request was badly formatted. For example, the provided study instance identifier did not conform the expected UID format or the requested transfer-syntax encoding is not supported. | -| 401 (Unauthorized) | The client is not authenticated. | -| 403 (Forbidden) | The user isn't authorized. | -| 404 (Not Found) | The specified DICOM resource could not be found or for rendered request the instance did not contain pixel data | -| 406 (Not Acceptable) | The specified `Accept` header is not supported or for rendered and transcode requests the file requested was too large | -| 503 (Service Unavailable) | The service is unavailable or busy. Please try again later. | - -## Search (QIDO-RS) - -Query based on ID for DICOM Objects (QIDO) enables you to search for studies, series and instances by attributes. - -| Method | Path | Description | -| :----- | :---------------------------------------------- | :-------------------------------- | -| *Search for Studies* | -| GET | ../studies?... | Search for studies | -| *Search for Series* | -| GET | ../series?... | Search for series | -| GET |../studies/{study}/series?... | Search for series in a study | -| *Search for Instances* | -| GET |../instances?... | Search for instances | -| GET |../studies/{study}/instances?... | Search for instances in a study | -| GET |../studies/{study}/series/{series}/instances?... | Search for instances in a series | - -The following `Accept` header(s) are supported for searching: - -- `application/dicom+json` - -### Supported Search Parameters - -The following parameters for each query are supported: - -| Key | Support Value(s) | Allowed Count | Description | -| :--------------- | :---------------------------- | :------------ | :---------- | -| `{attributeID}=` | {value} | 0...N | Search for attribute/ value matching in query. | -| `includefield=` | `{attributeID}`
`all` | 0...N | The additional attributes to return in the response. Both, public and private tags are supported.
When `all` is provided, please see [Search Response](###Search-Response) for more information about which attributes will be returned for each query type.
If a mixture of {attributeID} and 'all' is provided, the server will default to using 'all'. | -| `limit=` | {value} | 0..1 | Integer value to limit the number of values returned in the response.
Value can be between the range 1 >= x <= 200. Defaulted to 100. | -| `offset=` | {value} | 0..1 | Skip {value} results.
If an offset is provided larger than the number of search query results, a 204 (no content) response will be returned. | -| `fuzzymatching=` | `true` \| `false` | 0..1 | If true fuzzy matching is applied to PatientName attribute. It will do a prefix word match of any name part inside PatientName value. For example, if PatientName is "John^Doe", then "joh", "do", "jo do", "Doe" and "John Doe" will all match. However "ohn" will not match. | - -#### Searchable Attributes - -We support searching on below attributes and search type. - -| Attribute Keyword | All Studies | All Series | All Instances | Study's Series | Study's Instances | Study Series' Instances | -| :---------------- | :---: | :----: | :------: | :---: | :----: | :------: | -| StudyInstanceUID | X | X | X | | | | -| PatientName | X | X | X | | | | -| PatientID | X | X | X | | | | -| PatientBirthDate | X | X | X | | | | -| AccessionNumber | X | X | X | | | | -| ReferringPhysicianName | X | X | X | | | | -| StudyDate | X | X | X | | | | -| StudyDescription | X | X | X | | | | -| ModalitiesInStudy | X | X | X | | | | -| SeriesInstanceUID | | X | X | X | X | | -| Modality | | X | X | X | X | | -| PerformedProcedureStepStartDate | | X | X | X | X | | -| ManufacturerModelName | | X | X | X | X | | -| SOPInstanceUID | | | X | | X | X | - -#### Search Matching - -We support below matching types. - -| Search Type | Supported Attribute | Example | -| :---------- | :------------------ | :------ | -| Range Query | StudyDate, PatientBirthDate | {attributeID}={value1}-{value2}. For date/ time values, we support an inclusive range on the tag. This will be mapped to `attributeID >= {value1} AND attributeID <= {value2}`. If {value1} is not specified, all occurrences of dates/times prior to and including {value2} will be matched. Likewise, if {value2} is not specified, all occurrences of {value1} and subsequent dates/times will be matched. However, one of these values has to be present. `{attributeID}={value1}-` and `{attributeID}=-{value2}` are valid, however, `{attributeID}=-` is invalid. | -| Exact Match | All supported attributes | {attributeID}={value1} | -| Fuzzy Match | PatientName, ReferringPhysicianName | Matches any component of the name which starts with the value. | - -#### Attribute ID - -Tags can be encoded in a number of ways for the query parameter. We have partially implemented the standard as defined in [PS3.18 6.7.1.1.1](http://dicom.nema.org/medical/dicom/2019a/output/chtml/part18/sect_6.7.html#sect_6.7.1.1.1). The following encodings for a tag are supported: - -| Value | Example | -| :--------------- | :--------------- | -| {group}{element} | 0020000D | -| {dicomKeyword} | StudyInstanceUID | - -Example query searching for instances: -`../instances?Modality=CT&00280011=512&includefield=00280010&limit=5&offset=0` - -### Search Response - -The response will be an array of DICOM datasets. Depending on the resource, by *default* the following attributes are returned: - -#### Default Study tags - -| Tag | Attribute Name | -| :----------- | :------------- | -| (0008, 0005) | SpecificCharacterSet | -| (0008, 0020) | StudyDate | -| (0008, 0030) | StudyTime | -| (0008, 0050) | AccessionNumber | -| (0008, 0056) | InstanceAvailability | -| (0009, 0090) | ReferringPhysicianName | -| (0008, 0201) | TimezoneOffsetFromUTC | -| (0010, 0010) | PatientName | -| (0010, 0020) | PatientID | -| (0010, 0030) | PatientBirthDate | -| (0010, 0040) | PatientSex | -| (0020, 0010) | StudyID | -| (0020, 000D) | StudyInstanceUID | - -#### Default Series tags - -| Tag | Attribute Name | -| :----------- | :------------- | -| (0008, 0005) | SpecificCharacterSet | -| (0008, 0060) | Modality | -| (0008, 0201) | TimezoneOffsetFromUTC | -| (0008, 103E) | SeriesDescription | -| (0020, 000E) | SeriesInstanceUID | -| (0040, 0244) | PerformedProcedureStepStartDate | -| (0040, 0245) | PerformedProcedureStepStartTime | -| (0040, 0275) | RequestAttributesSequence | - -#### Default Instance tags - -| Tag | Attribute Name | -| :----------- | :------------- | -| (0008, 0005) | SpecificCharacterSet | -| (0008, 0016) | SOPClassUID | -| (0008, 0018) | SOPInstanceUID | -| (0008, 0056) | InstanceAvailability | -| (0008, 0201) | TimezoneOffsetFromUTC | -| (0020, 0013) | InstanceNumber | -| (0028, 0010) | Rows | -| (0028, 0011) | Columns | -| (0028, 0100) | BitsAllocated | -| (0028, 0008) | NumberOfFrames | - -If `includefield=all`, below attributes are included along with default attributes. Along with default attributes, this is the full list of attributes supported at each resource level. - -#### Additional Study tags - -| Tag | Attribute Name | -| :----------- | :------------- | -| (0008, 1030) | Study Description | -| (0008, 0063) | AnatomicRegionsInStudyCodeSequence | -| (0008, 1032) | ProcedureCodeSequence | -| (0008, 1060) | NameOfPhysiciansReadingStudy | -| (0008, 1080) | AdmittingDiagnosesDescription | -| (0008, 1110) | ReferencedStudySequence | -| (0010, 1010) | PatientAge | -| (0010, 1020) | PatientSize | -| (0010, 1030) | PatientWeight | -| (0010, 2180) | Occupation | -| (0010, 21B0) | AdditionalPatientHistory | - -#### Additional Series tags - -| Tag | Attribute Name | -| :----------- | :------------- | -| (0020, 0011) | SeriesNumber | -| (0020, 0060) | Laterality | -| (0008, 0021) | SeriesDate | -| (0008, 0031) | SeriesTime | - -The following attributes are returned: - -- All the match query parameters and UIDs in the resource url. -- `IncludeField` attributes supported at that resource level. -- If the target resource is `All Series`, then `Study` level attributes are also returned. -- If the target resource is `All Instances`, then `Study` and `Series` level attributes are also returned. -- If the target resource is `Study's Instances`, then `Series` level attributes are also returned. -- `NumberOfStudyRelatedInstances` aggregated attribute is supported in `Study` level includeField. -- `NumberOfSeriesRelatedInstances` aggregated attribute is supported in `Series` level includeField. - -### Search Response Codes - -The query API will return one of the following status codes in the response: - -| Code | Description | -| :------------------------ | :---------- | -| 200 (OK) | The response payload contains all the matching resource. | -| 204 (No Content) | The search completed successfully but returned no results. | -| 400 (Bad Request) | The server was unable to perform the query because the query component was invalid. Response body contains details of the failure. | -| 401 (Unauthorized) | The client is not authenticated. | -| 403 (Forbidden) | The user isn't authorized. | -| 503 (Service Unavailable) | The service is unavailable or busy. Please try again later. | - -### Additional Notes - -- Querying using the `TimezoneOffsetFromUTC` (`00080201`) is not supported. -- The query API will not return 413 (request entity too large). If the requested query response limit is outside of the acceptable range, a bad request will be returned. Anything requested within the acceptable range, will be resolved. -- When target resource is Study/Series there is a potential for inconsistent study/series level metadata across multiple instances. For example, two instances could have different patientName. In this case latest will win and you can search only on the latest data. -- Paged results are optimized to return matched *newest* instance first, this may result in duplicate records in subsequent pages if newer data matching the query was added. -- Matching is case in-sensitive and accent in-sensitive for PN VR types. -- Matching is case in-sensitive and accent sensitive for other string VR types. -- Only the first value will be indexed of a single valued data element that incorrectly has multiple values. - -## Delete - -This transaction is not part of the official DICOMweb™ Standard. It uses the DELETE method to remove representations of Studies, Series, and Instances from the store. - -| Method | Path | Description | -| :----- | :------------------------------------------------------ | :---------- | -| DELETE | ../studies/{study} | Delete all instances for a specific study. | -| DELETE | ../studies/{study}/series/{series} | Delete all instances for a specific series within a study. | -| DELETE | ../studies/{study}/series/{series}/instances/{instance} | Delete a specific instance within a series. | - -Parameters `study`, `series` and `instance` correspond to the DICOM attributes StudyInstanceUID, SeriesInstanceUID and SopInstanceUID respectively. - -There are no restrictions on the request's `Accept` header, `Content-Type` header or body content. - -> Note: After a Delete transaction the deleted instances will not be recoverable. - -### Response Status Codes - -| Code | Description | -| :--------------------------- | :---------- | -| 204 (No Content) | When all the SOP instances have been deleted. | -| 400 (Bad Request) | The request was badly formatted. | -| 401 (Unauthorized) | The client is not authenticated. | -| 401 (Unauthorized) | The client isn't authenticated. | -| 403 (Forbidden) | The user isn't authorized. | -| 404 (Not Found) | When the specified series was not found within a study, or the specified instance was not found within the series. | -| 503 (Service Unavailable) | The service is unavailable or busy. Please try again later. | - -### Delete Response Payload - -The response body will be empty. The status code is the only useful information returned. - -# Worklist Service (UPS-RS) - -The DICOM service supports the Push and Pull SOPs of the [Worklist Service (UPS-RS)](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#chapter_11). This service provides access to one Worklist containing Workitems, each of which represents a Unified Procedure Step (UPS). - -Throughout, the variable `{workitem}` in a URI template stands for a Workitem UID. - -## Create Workitem - -This transaction uses the POST method to create a new Workitem. - -| Method | Path | Description | -| :----- | :----------------- | :---------- | -| POST | `../workitems` | Create a Workitem. | -| POST | `../workitems?{workitem}` | Creates a Workitem with the specified UID. | - - -If not specified in the URI, the payload dataset must contain the Workitem in the SOPInstanceUID attribute. - -The `Accept` and `Content-Type` headers are required in the request, and must both have the value `application/dicom+json`. - -There are a number of requirements related to DICOM data attributes in the context of a specific transaction. Attributes may be -required to be present, required to not be present, required to be empty, or required to not be empty. These requirements can be -found in [this table](https://dicom.nema.org/medical/dicom/current/output/html/part04.html#table_CC.2.5-3). - -Notes on dataset attributes: -- **SOP Instance UID:** Although the reference table above says that SOP Instance UID should not be present, this guidance is specific to the DIMSE protocol and is -handled diferently in DICOMWeb™. SOP Instance UID **should be present** in the dataset if not in the URI. -- **Conditional requirement codes:** All the conditional requirement codes including 1C and 2C are treated as optional. - -### Create Response Status Codes - -| Code | Description | -| :--------------------------- | :---------- | -| 201 (Created) | The target Workitem was successfully created. | -| 400 (Bad Request) | There was a problem with the request. For example, the request payload did not satisfy the requirements above. | -| 401 (Unauthorized) | The client is not authenticated. | -| 403 (Forbidden) | The user isn't authorized. | -| 409 (Conflict) | The Workitem already exists. | -| 415 (Unsupported Media Type) | The provided `Content-Type` is not supported. | -| 503 (Service Unavailable) | The service is unavailable or busy. Please try again later. | - -### Create Response Payload - -A success response will have no payload. The `Location` and `Content-Location` response headers will contain -a URI reference to the created Workitem. - -A failure response payload will contain a message describing the failure. - -## Request Cancellation - -This transaction enables the user to request cancellation of a non-owned Workitem. - -There are -[four valid Workitem states](https://dicom.nema.org/medical/dicom/current/output/html/part04.html#table_CC.1.1-1): -- `SCHEDULED` -- `IN PROGRESS` -- `CANCELED` -- `COMPLETED` - -This transaction will only succeed against Workitems in the `SCHEDULED` state. Any user can claim ownership of a Workitem by -setting its Transaction UID and changing its state to `IN PROGRESS`. From then on, a user can only modify the Workitem by providing -the correct Transaction UID. While UPS defines Watch and Event SOP classes that allow cancellation requests and other events to be -forwarded, this DICOM service does not implement these classes, and so cancellation requests on workitems that are `IN PROGRESS` will -return failure. An owned Workitem can be canceled via the [Change Workitem State](#change-workitem-state) transaction. - -| Method | Path | Description | -| :------ | :---------------------------------------------- | :----------------------------------------------- | -| POST | ../workitems/{workitem}/cancelrequest | Request the cancellation of a scheduled Workitem | - -The `Content-Type` headers is required, and must have the value `application/dicom+json`. - -The request payload may include Action Information as [defined in the DICOM Standard](https://dicom.nema.org/medical/dicom/current/output/html/part04.html#table_CC.2.2-1). - -### Request Cancellation Response Status Codes - -| Code | Description | -| :--------------------------- | :---------- | -| 202 (Accepted) | The request was accepted by the server, but the Target Workitem state has not necessarily changed yet. | -| 400 (Bad Request) | There was a problem with the syntax of the request. | -| 401 (Unauthorized) | The client is not authenticated. | -| 403 (Forbidden) | The user isn't authorized. | -| 404 (Not Found) | The Target Workitem was not found. | -| 409 (Conflict) | The request is inconsistent with the current state of the Target Workitem. For example, the Target Workitem is in the SCHEDULED or COMPLETED state. | -| 415 (Unsupported Media Type) | The provided `Content-Type` is not supported. | -| 503 (Service Unavailable) | The service is unavailable or busy. Please try again later. | - -### Request Cancellation Response Payload - -A success response will have no payload, and a failure response payload will contain a message describing the failure. -If the Workitem Instance is already in a canceled state, the response will include the following HTTP Warning header: -`299: The UPS is already in the requested state of CANCELED.` - - -## Retrieve Workitem - -This transaction retrieves a Workitem. It corresponds to the UPS DIMSE N-GET operation. - -Refer: https://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_11.5 - -If the Workitem exists on the origin server, the Workitem shall be returned in an Acceptable Media Type. The returned Workitem shall not contain the Transaction UID (0008,1195) Attribute. This is necessary to preserve this Attribute's role as an access lock. - -| Method | Path | Description | -| :------ | :---------------------- | :------------ | -| GET | ../workitems/{workitem} | Request to retrieve a Workitem | - -The `Accept` header is required, and must have the value `application/dicom+json`. - -### Retrieve Workitem Response Status Codes - -| Code | Description | -| :---------------------------- | :---------- | -| 200 (OK) | Workitem Instance was successfully retrieved. | -| 400 (Bad Request) | There was a problem with the request. | -| 401 (Unauthorized) | The client is not authenticated. | -| 403 (Forbidden) | The user isn't authorized. | -| 404 (Not Found) | The Target Workitem was not found. | -| 503 (Service Unavailable) | The service is unavailable or busy. Please try again later. | - -### Retrieve Workitem Response Payload - -* A success response has a single part payload containing the requested Workitem in the Selected Media Type. -* The returned Workitem shall not contain the Transaction UID (0008,1195) Attribute of the Workitem, since that should only be known to the Owner. - -## Update Workitem - -This transaction modifies attributes of an existing Workitem. It corresponds to the UPS DIMSE N-SET operation. - -Refer: https://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_11.6 - -To update a Workitem currently in the SCHEDULED state, the Transaction UID Attribute shall not be present. For a Workitem in the IN PROGRESS state, the request must include the current Transaction UID as a query parameter. If the Workitem is already in the COMPLETED or CANCELED states, the response will be 400 (Bad Request). - -| Method | Path | Description | -| :------ | :------------------------------ | :-------------------- | -| POST | ../workitems/{workitem}?{transaction-uid} | Update Workitem Transaction | - -The `Content-Type` header is required, and must have the value `application/dicom+json`. - -The request payload contains a dataset with the changes to be applied to the target Workitem. When modifying a sequence, the request must include all Items in the sequence, not just the Items to be modified. -When multiple Attributes need updating as a group, do this as multiple Attributes in a single request, not as multiple requests. - -There are a number of requirements related to DICOM data attributes in the context of a specific transaction. Attributes may be -required to be present, required to not be present, required to be empty, or required to not be empty. These requirements can be -found in [this table](https://dicom.nema.org/medical/dicom/current/output/html/part04.html#table_CC.2.5-3). - -Notes on dataset attributes: -- **Conditional requirement codes:** All the conditional requirement codes including 1C and 2C are treated as optional. - -The request cannot set the value of the Procedure Step State (0074,1000) Attribute. Procedure Step State is managed using the Change State transaction, or the Request Cancellation transaction. - -### Update Workitem Transaction Response Status Codes -| Code | Description | -| :---------------------------- | :---------- | -| 200 (OK) | The Target Workitem was updated. | -| 400 (Bad Request) | There was a problem with the request. For example: (1) the Target Workitem was in the COMPLETED or CANCELED state. (2) the Transaction UID is missing. (3) the Transaction UID is incorrect. (4) the dataset did not conform to the requirements. -| 401 (Unauthorized) | The client is not authenticated. | -| 403 (Forbidden) | The user isn't authorized. | -| 404 (Not Found) | The Target Workitem was not found. | -| 409 (Conflict) | The request is inconsistent with the current state of the Target Workitem. | -| 415 (Unsupported Media Type) | The provided `Content-Type` is not supported. | -| 503 (Service Unavailable) | The service is unavailable or busy. Please try again later. | - -### Update Workitem Transaction Response Payload -The origin server shall support header fields as required in [Table 11.6.3-2](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#table_11.6.3-2). - -A success response shall have either no payload, or a payload containing a Status Report document. - -A failure response payload may contain a Status Report describing any failures, warnings, or other useful information. - -## Change Workitem State - -This transaction is used to change the state of a Workitem. It corresponds to the UPS DIMSE N-ACTION operation "Change UPS State". State changes are used to claim ownership, complete, or cancel a Workitem. - -Refer: https://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_11.7 - -If the Workitem exists on the origin server, the Workitem shall be returned in an Acceptable Media Type. The returned Workitem shall not contain the Transaction UID (0008,1195) Attribute. This is necessary to preserve this Attribute's role as an access lock as described [here.](https://dicom.nema.org/medical/dicom/current/output/html/part04.html#sect_CC.1.1) - -| Method | Path | Description | -| :------ | :------------------------------ | :-------------------- | -| PUT | ../workitems/{workitem}/state | Change Workitem State | - -The `Accept` header is required, and must have the value `application/dicom+json`. - -The request payload shall contain the Change UPS State Data Elements. These data elements are: - -* **Transaction UID (0008,1195)** -The request payload shall include a Transaction UID. The user agent creates the Transaction UID when requesting a transition to the IN PROGRESS state for a given Workitem. The user agent provides that Transaction UID in subsequent transactions with that Workitem. - -* **Procedure Step State (0074,1000)** -The legal values correspond to the requested state transition. They are: "IN PROGRESS", "COMPLETED", or "CANCELED". - - -### Change Workitem State Response Status Codes - -| Code | Description | -| :---------------------------- | :---------- | -| 200 (OK) | Workitem Instance was successfully retrieved. | -| 400 (Bad Request) | The request cannot be performed for one of the following reasons: (1) the request is invalid given the current state of the Target Workitem. (2) the Transaction UID is missing. (3) the Transaction UID is incorrect -| 401 (Unauthorized) | The client is not authenticated. | -| 403 (Forbidden) | The user isn't authorized. | -| 404 (Not Found) | The Target Workitem was not found. | -| 409 (Conflict) | The request is inconsistent with the current state of the Target Workitem. | -| 503 (Service Unavailable) | The service is unavailable or busy. Please try again later. | - -### Change Workitem State Response Payload - -* Responses will include the header fields specified in [section 11.7.3.2](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_11.7.3.2) -* A success response shall have no payload. -* A failure response payload may contain a Status Report describing any failures, warnings, or other useful information. - -## Search Workitems - -This transaction enables you to search for Workitems by attributes. - -| Method | Path | Description | -| :----- | :---------------------------------------------- | :-------------------------------- | -| GET | ../workitems? | Search for Workitems | - -The following `Accept` header(s) are supported for searching: - -- `application/dicom+json` - -### Supported Search Parameters - -The following parameters for each query are supported: - -| Key | Support Value(s) | Allowed Count | Description | -| :--------------- | :---------------------------- | :------------ | :---------- | -| `{attributeID}=` | {value} | 0...N | Search for attribute/ value matching in query. | -| `includefield=` | `{attributeID}`
`all` | 0...N | The additional attributes to return in the response. Only top-level attributes can be specified to be included - not attributes that are part of sequences. Both public and private tags are supported.
When `all` is provided, please see [Search Response](###Search-Response) for more information about which attributes will be returned for each query type.
If a mixture of {attributeID} and 'all' is provided, the server will default to using 'all'. | -| `limit=` | {value} | 0...1 | Integer value to limit the number of values returned in the response.
Value can be between the range 1 >= x <= 200. Defaulted to 100. | -| `offset=` | {value} | 0...1 | Skip {value} results.
If an offset is provided larger than the number of search query results, a 204 (no content) response will be returned. | -| `fuzzymatching=` | `true` \| `false` | 0...1 | If true fuzzy matching is applied to any attributes with the Person Name (PN) Value Representation (VR). It will do a prefix word match of any name part inside these attributes. For example, if PatientName is "John^Doe", then "joh", "do", "jo do", "Doe" and "John Doe" will all match. However "ohn" will not match. | - -#### Searchable Attributes - -We support searching on these attributes: - -| Attribute Keyword | -| :---------------- | -| PatientName | -| PatientID | -| ReferencedRequestSequence.AccessionNumber | -| ReferencedRequestSequence.RequestedProcedureID | -| ScheduledProcedureStepStartDateTime | -| ScheduledStationNameCodeSequence.CodeValue | -| ScheduledStationClassCodeSequence.CodeValue | -| ScheduledStationGeographicLocationCodeSequence.CodeValue | -| ProcedureStepState | -| StudyInstanceUID | - -#### Search Matching - -We support these matching types: - -| Search Type | Supported Attribute | Example | -| :---------- | :------------------ | :------ | -| Range Query | Scheduled​Procedure​Step​Start​Date​Time | {attributeID}={value1}-{value2}. For date/ time values, we support an inclusive range on the tag. This will be mapped to `attributeID >= {value1} AND attributeID <= {value2}`. If {value1} is not specified, all occurrences of dates/times prior to and including {value2} will be matched. Likewise, if {value2} is not specified, all occurrences of {value1} and subsequent dates/times will be matched. However, one of these values has to be present. `{attributeID}={value1}-` and `{attributeID}=-{value2}` are valid, however, `{attributeID}=-` is invalid. | -| Exact Match | All supported attributes | {attributeID}={value1} | -| Fuzzy Match | PatientName | Matches any component of the name which starts with the value. | - -> Note: While we do not support full sequence matching, we do support exact match on the attributes listed above that are contained in a sequence. - -#### Attribute ID - -Tags can be encoded in a number of ways for the query parameter. We have partially implemented the standard as defined in [PS3.18 6.7.1.1.1](http://dicom.nema.org/medical/dicom/2019a/output/chtml/part18/sect_6.7.html#sect_6.7.1.1.1). The following encodings for a tag are supported: - -| Value | Example | -| :--------------- | :--------------- | -| {group}{element} | 00100010 | -| {dicomKeyword} | PatientName | - -Example query: **../workitems?PatientID=K123&0040A370.00080050=1423JS&includefield=00404005&limit=5&offset=0** - -### Search Response - -The response will be an array of 0...N DICOM datasets. The following attributes are returned: - - - All attributes in [DICOM PS 3.4 Table CC.2.5-3](https://dicom.nema.org/medical/dicom/current/output/html/part04.html#table_CC.2.5-3) with a Return Key Type of 1 or 2. - - All attributes in [DICOM PS 3.4 Table CC.2.5-3](https://dicom.nema.org/medical/dicom/current/output/html/part04.html#table_CC.2.5-3) with a Return Key Type of 1C for which the conditional requirements are met. - - All other Workitem attributes passed as match parameters. - - All other Workitem attributes passed as includefield parameter values. - -### Search Response Codes - -The query API will return one of the following status codes in the response: - -| Code | Description | -| :------------------------ | :---------- | -| 200 (OK) | The response payload contains all the matching resource. | -| 206 (Partial Content) | The response payload contains only some of the search results, and the rest can be requested through the appropriate request. | -| 204 (No Content) | The search completed successfully but returned no results. | -| 400 (Bad Request) | The was a problem with the request. For example, invalid Query Parameter syntax. Response body contains details of the failure. | -| 401 (Unauthorized) | The client is not authenticated. | -| 403 (Forbidden) | The user isn't authorized. | -| 503 (Service Unavailable) | The service is unavailable or busy. Please try again later. | - -### Additional Notes - -- The query API will not return 413 (request entity too large). If the requested query response limit is outside of the acceptable range, a bad request will be returned. Anything requested within the acceptable range, will be resolved. -- Paged results are optimized to return matched *newest* instance first, this may result in duplicate records in subsequent pages if newer data matching the query was added. -- Matching is case insensitive and accent insensitive for PN VR types. -- Matching is case insensitive and accent sensitive for other string VR types. -- If there is a scenario where canceling a Workitem and querying the same happens at the same time, then the query will most likely exclude the Workitem that is getting updated and the response code will be 206 (Partial Content). diff --git a/docs/resources/dicom-server-maintaince-guide.md b/docs/resources/dicom-server-maintaince-guide.md deleted file mode 100644 index 16a4572e90..0000000000 --- a/docs/resources/dicom-server-maintaince-guide.md +++ /dev/null @@ -1,22 +0,0 @@ -# Managing deployment of Medical Imaging Server for DICOM in production - -Below is a list of things to consider if you want to mantain your own deployment. We recommend you to be moderately familiar with the code. - -| Area | Description | -| --- | --- | -| ARM Deployment template | The [sample deployment](../quickstarts/deploy-via-azure.md) deploys all the dependencies and configurations, but is not production ready. Consider it a template. | -| Web Application | This is our hosting layer. Consider creating your own with the right composition of Authentication and Authorization services. The [Web application](../../src/Microsoft.Health.Dicom.Web/) we have uses development Identity server which must be replaced for production use. | -| Authentication and Authorization | Sample Azure deployment will deploy with no security, meaning the server is accessible to everyone on the internet. Review our Authentication and Authorization documentation and set it up correctly.| -| Upgrade | This is a active project. We are constantly fixing issues, adding features and re-architecting to make the service better. This means both [SQL schema](https://github.com/microsoft/fhir-server/blob/main/docs/SchemaMigrationGuide.md) and binaries must to be upgraded regularly. Our SQL schema versions go out support quickly. A weekly upgrade cadence, with a close monitoring of commit history, is recommended. | -| Monitoring | Logs from the DICOM server optionally go to Application Insights. Consider adding active monitoring and alerting. | -| Capacity and Scale | Plan for expected production workloads and performance needs. Azure SQL Database and Azure App Service scale independently. Consider both horizontal and vertical scaling. | -| Network Security | Consider network security like Private endpoints and Virtual network access to the dependent services. This can provide additional security features for defense in depth. | -| Pricing | A basic App Service plan and SQL Database is deployed with the sample deployment. There are basic charges for these resources even if they are not used. | -| Data redundancy | Ensure the correct SQL Database and Storage Account SKU to provide for desired data redudancy and availability. | -| Disaster recovery | Consider multi-region failover, back-ups and other techniques to support mission critical deployments. | -| Privacy | Ensure up-to-date policies, tools, and resources to be compliant with Privacy. | -| Compliance | Application Audit logs, Security scan, Data access management, Secure development life cycle, Secret management, access review... etc are some of the tools and process you will have to consider to store PHI data in HIPPA, HITRUST and ISO certified way. | - -
- -We recommend trying [Azure Health Data Services](https://azure.microsoft.com/en-us/services/health-data-services/#overview) to get up-and-running quickly for production workloads. Our managed service takes care of all of the aforementioned complexity with a guaranteed SLA. Learn more about our managed DICOM Server offering [here](https://docs.microsoft.com/en-us/azure/healthcare-apis/dicom/dicom-services-overview). diff --git a/docs/resources/faq.md b/docs/resources/faq.md deleted file mode 100644 index 51f415f87c..0000000000 --- a/docs/resources/faq.md +++ /dev/null @@ -1,57 +0,0 @@ -# Frequently asked questions about the Medical Imaging Server for DICOM - -## What is the Medical Imaging Server for DICOM? - -The Medical Imaging Server for DICOM is an open source DICOMweb™ server that is easily deployed on Azure. It allows standards-based communication with any DICOMweb™ enabled systems for data exchange, and introduces integration between DICOM and FHIR. The server identifies and extracts DICOM metadata and injects it into a FHIR endpoint (such as the Azure API for FHIR or FHIR Server for Azure) to create a holistic view of patient data. - -## What are the key requirements to use the Medical Imaging Server for DICOM? - -The Medical Imaging Server for DICOM needs an Azure subscription to configure and run the required components. These components are, by default, created inside of an existing or new Azure Resource Group to simplify management. Additionally, an Azure Active Directory account is required. - -## Where is the data persisted using the Medical Imaging Server for DICOM? - -The customer controls all of the data persisted by the Medical Imaging Server for DICOM. The following components are used to persist data: -- Blob storage: persists all DICOM data and metadata -- Azure SQL: indexes a subset of the DICOM metadata to support queries, and maintains a queryable log of changes -- Azure Key Vault: stores critical security information - -## What data formats are compatible with the Medical Imaging Server for DICOM? - -The Medical Imaging Server for DICOM exposes a REST API that is compatible with the [DICOMweb™ Standards](https://www.dicomstandard.org/dicomweb/)specified and maintained by NEMA. - -The server does not support DICOM DIMSE, which works primarily over a local area network and is unsuited for modern internet-based APIs. DIMSE is an incredibly popular standard used by nearly all medical imaging devices to communicate with other components of a provider’s medical imaging solution, such as PACS (Picture Archiving and Communication Systems) and medical imaging viewers. However, many modern systems, especially PACS and viewers, have begun to also support the related (and compatible) DICOMweb™ Standard. For those systems which only speak DICOM DIMSE there are adapters available which allow for seamless communication between the local DIMSE-supporting systems and the Medical Imaging Server for DICOM. - -## What version of DICOM does the Medical Imaging Server for DICOM support? - -The DICOM standards has been fixed at version 3.0 since 1993. However, the standard continues to add both breaking and non-breaking changes through various workgroups. - -No single product, including the Medical Imaging Server for DICOM, fully supports the DICOM standard. Instead, each product includes a DICOM Conformance document that specifies exactly what is supported. (Unsupported features are traditionally not called out explicitly.) The Conformance document is available [here](conformance-statement.md). - -## What is REST API versioning in the Medical Imaging Server for DICOM? - -The Medical Imaging Server for DICOM has versions for the REST API used to access the server. The version is specified as a URL path in the requests. For more information visit the [Api Versioning Documentation](../api-versioning.md). - -## Does the Medical Imaging Server for DICOM store any PHI? - -Absolutely. One of the core objectives for the Medical Imaging Server for DICOM is to support standard and innovating radiologist workflows. These workflows demand the use of PHI data. - -## How does the Medical Imaging Server for DICOM maitain privacy and security? - -Trust, data privacy, and security are the highest priority for Microsoft and remain fundamental to managing PHI data in the cloud. The Medical Imaging Server for DICOM is designed to support security and privacy. The OSS code maps structured metadata from DICOM images to the FHIR data framework which allows for downstream exchange of data via FHIR APIS. - -Microsoft Azure offers a comprehensive set of offerings to help your organization comply with national, regional, and industry-specific requirements governing the collection and use of data. [Learn more about Azure compliance](https://azure.microsoft.com/overview/trusted-cloud/compliance/). - -## What is DICOM? - -DICOM (Digital Imaging and Communications in Medicine) is the international standard to transmit, store, retrieve, print, process, and display medical imaging information, and is the primary medical imaging standard accepted across healthcare. Although some exceptions exist (dentistry, veterinary), nearly all medical specialties, equipment manufacturers, software vendors and individual practitioners rely on DICOM at some stage of any medical workflow involving imaging. DICOM ensures that medical images meet quality standards, so that the accuracy of diagnosis can be preserved. Most imaging modalities, including CT, MRI and ultrasound must conform to the DICOM standards. Images that are in the DICOM format need to be accessed and used through specialized DICOM applications. - - -## What is the difference between Retrieve, Query & Store? - -Query, Retrieve, and store are standard DICOMweb™ verbs. Query (QIDO) searches for DICOM objects. QIDO enables you to search for studies, series and instances by patient ID. Retrieve (WADO) enables you to retrieve specific studies, series and instances by reference. Store (STOW-RS) enables you to store specific instances to a DICOM server. - -You can learn more about the specifics of QIDO, WADO and STOW from [DICOMweb™](https://www.dicomstandard.org/dicomweb). - -## What versions of .NET are Supported? - -The web server always supports the latest .NET version, whether it is a Standard Term Support (STS) or Long Term Support (LTS) version. The Azure Functions on the other hand, used to execute long-running operations, support the latest LTS version of .NET as per the support provided the Azure Functions framework. The Medical Imaging Server for DICOM docker images always include the necessary runtime components. diff --git a/docs/resources/health-check-api.md b/docs/resources/health-check-api.md deleted file mode 100644 index 322ee4fde5..0000000000 --- a/docs/resources/health-check-api.md +++ /dev/null @@ -1,87 +0,0 @@ -# Health Check API - -The Health Check API allows user to check the health of the Medical Imaging Server for DICOM and all the underlying services. - -## API Design - -The Health Check API exposes a GET endpoint and responds with JSON content. - -Verb | Route | Returns -:--- | :----------------- | :---------- -GET | /health/check | Json Object - -## Object Model - -The GET request returns a JSON object with the following fields: - -Field | Type | Description -:------------ | :----- | :---------- -overallStatus | string | Status `Healthy` or `Unhealthy` -details | array | Array of objects with details on underlying services - -Objects of the `details` array have the following model: - -Field | Type | Description -:------------ | :----- | :---------- -name | string | Name of the service -status | string | Status `Healthy` or `Unhealthy` -description | string | Description of the status - -## Get Health Status - -Internally, the Microsoft.Extensions.Diagnostics.HealthChecks NuGet package is used for getting the health status. Its documentation can be found [here](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks?view=dotnet-plat-ext-3.1). - -To check the health status of Medical Imaging Server for DICOM, the user issues a GET request to /health/check. Following is a sample JSON response if all the underlying services are healthy: -``` -{ - "overallStatus":"Healthy", - "details": - [ - { - "name":"DicomBlobHealthCheck", - "status":"Healthy", - "description":"Successfully connected to the blob data store." - }, - { - "name":"MetadataHealthCheck", - "status":"Healthy", - "description":"Successfully connected to the blob data store." - }, - { - "name":"SqlServerHealthCheck", - "status":"Healthy", - "description":"Successfully connected to the data store." - } - ] -} -``` - -Healthy (HTTP Status Code 200) is returned as the overall status if all the underlying services are healthy. If any of the underlying services are unhealthy, overall status of the Medical Imaging Server for DICOM will be returned as unhealthy (HTTP Status Code 503). - -Following is an example JSON if SQL Server service is unhealthy: -``` -{ - "overallStatus":"Unhealthy", - "details": - [ - { - "name":"DicomBlobHealthCheck", - "status":"Healthy", - "description":"Successfully connected to the blob data store." - }, - { - "name":"MetadataHealthCheck", - "status":"Healthy", - "description":"Successfully connected to the blob data store." - }, - { - "name":"SqlServerHealthCheck", - "status":"Unhealthy", - "description":"Failed to connect to the data store." - } - ] -} -``` - -Details array in the response contains details of all the services and their health status. - diff --git a/docs/resources/performance-guidance.md b/docs/resources/performance-guidance.md deleted file mode 100644 index fa9cf65839..0000000000 --- a/docs/resources/performance-guidance.md +++ /dev/null @@ -1,70 +0,0 @@ -# Medical Imaging Server for DICOM Performance Guidance - -When you deploy an instance of the Medical Imaging Server for DICOM, the following resources are important for performance in a production workload: - -- **App Service Plan**: Hosts the Medical Imaging Service for DICOM. -- **Azure SQL**: Indexes a subset of the Medical Imaging Server for DICOM metadata to support queries and to maintain a queryable log of changes. -- **Storage Account**: Blob Storage which persists all Medical Imaging Server for DICOM data and metadata. - -This resource provides guidance on Azure App Service, SQL Database and Storage Account settings for the Medical Imaging Server for DICOM. Note, these are recommendations but may not fit the exact needs of your workload. - -## Azure App Service Plan - -The S1 tier is the default App Service Plan SKU enabled upon deployment. You can customize your App Service Plan SKU during deployment via the Medical Imaging Server for DICOM [Quickstart Deploy to Azure](../quickstarts/deploy-via-azure.md). You can also update your App Service Plan after deployment. You can find instructions to that at [Configure Medical Imaging Server for DICOM Settings](../how-to-guides/configure-dicom-server-settings.md). - -Azure offers a variety of plans to meet your workload requirements. To learn more about the various plans, view the [App Service pricing](https://azure.microsoft.com/pricing/details/app-service/windows/). To learn how to update your Azure App Service Plan, refer to [Configure Medical Imaging Server for DICOM Settings](../how-to-guides/configure-dicom-server-settings.md). - -## Azure SQL Database Tier - -The Standard tier of the DTU-based SQL performance tiers is enabled by default upon deployment. We recommend the DTU Purchase Model over the vCore model for the Medical Imaging Server for DICOM. In DTU-based SQL purchase models, a fixed set of resources is assigned to the database via performance tiers: Basic, Standard and Premium. - -To review the various SQL Database Tiers from Azure, refer to [Azure SQL Database Pricing](https://azure.microsoft.com/pricing/details/sql-database/single/). To learn how to update your Azure SQL Database tier, refer to [Configure Medical Imaging Server for DICOM Settings](../how-to-guides/configure-dicom-server-settings.md). - -## Geo-Redundancy - -For a production workload, we highly recommend configuring your Medical Imaging Server for DICOM to support geo-redundancy. - -### Geo-Redundant Azure Storage - -Azure Storage offers geo-redundant storage to ensure high availability even in the event of a regional outage. We highly recommend choosing an Azure Storage Account Sku that supports geo-redundancy if you are running a production workload. Azure storage offers two options for geo-redundant replication, Geo-zone-redundant-storage (GZRS) and Geo-redundant-storage (GRS). Refer to this article to decide which geo-redundant Azure Storage option is right for you: [Use geo-redundancy to design highly available applications](https://docs.microsoft.com/en-us/azure/storage/common/geo-redundant-design). - -You can customize your Azure Storage Account SKU during deployment via the Medical Imaging Server for DICOM [Quickstart Deploy to Azure](../quickstarts/deploy-via-azure.md). By default, Standard LRS is selected, which is Standard Locally Redundant Storage. - -### Geo-replication for SQL Database - -In addition to configuring geo-redundant Azure Storage, we recommend configuring active geo-replication for your Azure SQL Database. This allows you to create readable secondary databases of individual databases on a server in the same or different data center region. For a tutorial on how to configure this, see [Creating and using active geo-replication - Azure SQL Database](https://docs.microsoft.com/azure/azure-sql/database/active-geo-replication-overview). - -## Workload Scenarios for Azure SQL Database & Azure App Service - -### Scenario 1: Testing out DICOM Server - -If you are testing out the Medical Imaging Server for DICOM and not running production workloads, we recommend using the S1 Azure App Service Tier alongside a Standard Azure SQL Database (S1, S2, S3). For a small system that does not require redundancy, with these tiers, you can spend as little as ~$70/month on Azure App Service & Azure SQL Database. - -At these tiers with an appropriate mix of STOW, WADO and QIDO requests, you can expect to handle between 2,000 and 20,000 requests/minute and a response time under 1 second. Larger files, usage patterns that lean heavily to STOW, and poor bandwidth will reduce the number of requests per minute. - -### Scenario 2: Production workload for a hospital system - -For a production workload, we recommend scaling up your Azure SQL Database to S12. For your Azure App Service, any Standard tier should be sufficient. If you are going into production, you also need to ensure your Medical Imaging Server for DICOM supports geo-redundancy. Refer to our [geo-redundancy guidelines](##Geo-Redundancy). - -We recommend the S1 Standard Azure App Service Tier along side an Azure SQL Tier of S12. At these tiers with an appropriate mix of STOW, WADO and QIDO requests, you can expect to handle between 1,000 and 20,000 requests/minutes with response times under 400 ms. Larger files, usage patterns that lean heavily to STOW, and poor bandwidth will reduce the number of requests per minute. We recommend testing performance with your data and indented use. - -### Scenario 3: Bulk ingest of DICOM files - -If you workload requires bulk ingest of DICOM files or automated tooling to process DICOM files, we recommend scaling up your Azure App Service & Azure SQL Database to Premium tiers. If you are going into production, you also need to ensure your Medical Imaging Server for DICOM supports geo-redundancy. Refer to our [geo-redundancy guidelines](##Geo-Redundancy). - -To support a large number of DICOM transactions per day, we recommend a P1v2 tier for Azure App Service alongside a P11 tier for Azure SQL Database. With excellent bandwidth, relatively small images and and appropriate mix of STOW, WADO and QIDO requests, you can expect to handle between 40,000 and 100,000 requests/minutes with response times under 200 ms. Larger files, usage patterns that lean heavily to STOW, and poor bandwidth will reduce the number of requests per minute. We recommend testing performance with your data and indented use. - -### Comments about Performance Results - -In order to estimate workloads, automated scale tests were performed to simulate a production environment. The guidance in this document is a suggestion and may need to be modified to meet your environment. A few important notes to consider while configuring your service: - -- Once you cross ~60% usage of your database, you will start to see a decline in performance. -- The scale tests done for the guidance in this document were performed using 500 KB DICOM files. -- Additionally, the scale tests were running with multiple concurrent callers. At fewer concurrent callers, you may have a higher request/minute and response time. - -## Summary - -In this resource, we reviewed suggested guidance for Azure App Service tiers, Azure SQL tiers and Storage Account settings so that your Medical Imaging Server for DICOM can meet your workload requirements: - -- To get started with the Medical Imaging Server for DICOM, [Deploy to Azure](../quickstarts/deploy-via-azure.md). -- If you already have configured an instance of the Medical Imaging Server for DICOM, [Configure your DICOM Server Settings](../how-to-guides/configure-dicom-server-settings.md). diff --git a/docs/resources/schema-manager.md b/docs/resources/schema-manager.md deleted file mode 100644 index f36b156192..0000000000 --- a/docs/resources/schema-manager.md +++ /dev/null @@ -1,85 +0,0 @@ -# DICOM Schema Manager - -### What is it? -Schema Manager is a command line app that upgrades the schema in your database from one version to the next through migration scripts. - ------------- - -### How do you use it? -DICOM Schema Manager currently has one command (**apply**) with the following options: - -| Option | Description | -| ------------ | ------------ | -| `-cs, --connection-string` | The connection string of the SQL server to apply the schema update. (REQUIRED) | -| `-mici, --managed-identity-client-id` | The client ID of the managed identity to be used. | -| `-at, --authentication-type` | The authentication type to use. Valid values are `ManagedIdentity` and `ConnectionString`. | -| `-v, --version` | Applies all available versions from the current database version to the specified version. | -| `-n, --next` | Applies the next available database version. | -| `-l, --latest` | Applies all available versions from the current database version to the latest. | -| `-f, --force` | The schema migration is run without validating the specified version. | -| `-?, -h, --help` | Show help and usage information. | - -You can view the most up-to-date options by running the following command: -`.\Microsoft.Health.Dicom.SchemaManager.Console.exe apply -?` - -Example command line usage: -`.\Microsoft.Health.Dicom.SchemaManager.Console.exe apply --connection-string "server=(local);Initial Catalog=DICOM;TrustServerCertificate=True;Integrated Security=True" --version 20` - -`.\Microsoft.Health.Dicom.SchemaManager.Console.exe apply -cs "server=(local);Initial Catalog=DICOM;TrustServerCertificate=True;Integrated Security=True" --latest` - ------------- - -### Important Database Tables - -**SchemaVersion** -- This table holds all schema versions that have been applied to the database. - -**InstanceSchema** -- Each DICOM instance reports the schema version it is at, as well as the versions it is compatible with, to the InstanceSchema database table. - ------------- - -### Terminology - -**Current database version** -- The maximum SchemaVersion version in the database. - -**Current instance version** -- The maximum SchemaVersion version in the database that falls at or below the SchemaVersionConstants.Max value. For example, if the current database version is 25, but SchemaVersionConstants.Max is 23, the instance's current version will be 23. - -**Available version** -- Any version greater than the current database version. - -**Compatible version** -- Any version from SchemaVersionConstants.Min to SchemaVersionConstants.Max (inclusive). - ------------- - -### How does it work? - -Schema Manager runs through the following steps: -1. Verifies all arguments are supplied and valid. -2. Calls the [healthcare-shared-components ApplySchema function](https://github.com/microsoft/healthcare-shared-components/blob/main/src/Microsoft.Health.SqlServer/Features/Schema/Manager/SqlSchemaManager.cs#L53), which: - 1. Ensures the base schema exists. - 2. Ensures instance schema records exist. - 1. Since DICOM Server implements its own ISchemaClient (DicomSchemaClient), if there are no instance schema records, the upgrade continues uninterrupted. In healthcare-shared-components, this would throw an exception and cancel the upgrade. - 3. Gets all available versions and compares them against all compatible versions. - 4. Based on the current database schema version: - 1. If there is no version (base schema only), the latest full migration script is applied. - 2. If the current version is >= 1, each available version is applied one at a time until the database's schema version reaches the desired version input by the user (latest, next, or a specific version). - ------------- - -### Caveats - -Schema Manager works under the assumption that it will be updated at the same time as any DICOM binaries. It's possible to end up with a database in a bad state when running Schema Manager with a different tag version than the DICOM binary. For example, you could have a database upgraded to schema version 25, but the binary only supports up to schema version 23. - -Schema Manager is programmed to upgrade the database of an existing, running DICOM instance, or against a new database. If SchemaManager is run against an existing database with no running instances, SchemaManager will apply the latest SchemaVersion possible, and not take into account the compatibility from running instances. This is because the InstanceSchema table is only populated when DICOM services are running. - ------------- - -### SQL Script Locations - -- [Base Schema Script](https://github.com/microsoft/healthcare-shared-components/blob/main/src/Microsoft.Health.SqlServer/Features/Schema/Migrations/BaseSchema.sql) - -- [DICOM Migration Scripts](https://github.com/microsoft/dicom-server/tree/main/src/Microsoft.Health.Dicom.SqlServer/Features/Schema/Migrations) \ No newline at end of file diff --git a/docs/resources/swagger.md b/docs/resources/swagger.md deleted file mode 100644 index c0c5604673..0000000000 --- a/docs/resources/swagger.md +++ /dev/null @@ -1,102 +0,0 @@ -# Swagger - -We use [Swashbuckle](https://github.com/domaindrivendev/Swashbuckle.AspNetCore) to generate Swagger/ API Documentation. -If you've never worked with Swagger before, it may be helpful to -checkout [this sample](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/web-api-help-pages-using-swagger/samples/6.x/SwashbuckleSample) -and play with it. - -A quick summary of workflow is: - -- Make changes to an API or to some of the customizations or defaults we have for swagger -- Build the solution. The .dll generated is what is then used to generate the API documentation. -- Use `dotnet swagger` to generate documentation from the new .dll - -The DICOM OSS project is setup to autogenerate this documentation for you on build. - -## Customization and Defaulting - -Swagger and customizations are -set [here](https://github.com/microsoft/dicom-server/blob/main/src/Microsoft.Health.Dicom.Api/Registration/DicomServerServiceCollectionExtensions.cs#L133) -. - -We've written some customizations and added defaults. - -Defaults can be seen at src/Microsoft.Health.Dicom.Api/Configs/SwaggerConfiguration.cs. -Customizations can be seen at src/Microsoft.Health.Dicom.Api/Features/Swagger. - -Note that we also specify Licensing in src/Microsoft.Health.Dicom.Web/appsettings.json. If you take out the License -defaulting in SwaggerConfiguration.cs, appsettings.json will be used to get Licensing when using the post build hook. -However, these settings are not used if using `dotnet swagger` in your terminal, outside of a build. If anyone knows -why, please replace this content. - -## Updating Swagger YAML - -Swagger yaml will be generated for you on each build using a post build hook name `SwaggerPostBuildTarget` in -Microsoft.Health.Dicom.Web.csproj. - -### Add A New Version - -You can add a new version by adding a new `Exec MSBuild task` in `Microsoft.Health.Dicom.Web.csproj`. -Output should go to `swagger\\swagger.yaml` and with `` at the end of -the `dotnet swagger tofile` command, -specifying `name of the swagger doc you want to retrieve, as configured in your startup class`. - -Example: - -``` - -``` - -Be sure to also update the build/common/versioning.yml Powershell tasks to check for new versions. - -### ADO Checks - -We utilize [openapi-diff](https://github.com/OpenAPITools/openapi-diff) to check for differences and breaking API -changes. - -#### Checks For Latest Swagger - -As a way to ensure we always keep the swagger yaml up to date, there is a step in our ADO pipeline that will generate -swagger and error out if what is generated has differences from the yaml that was checked in. -This script lives in ./build/common/scripts/CheckForSwaggerChanges.ps1 - -You can run this script locally as well: - -``` -.\build\common\scripts\CheckForSwaggerChanges.ps1 -SwaggerDir 'swagger' -AssemblyDir 'src\Microsoft.Health.Dicom.Web\bin\x64\Debug\net6.0\Microsoft.Health.Dicom.Web.dll' -Version 'v1-prerelease','v1' -``` - -Note that this script does not use `dotnet swagger`'s comparison to detect changes as that only looks like API changes. -We want to compare the file as a whole, so we use Powershell's `Compare-Object` instead. - -#### Checks for Breaking APIChanges - -As a way to ensure we always consider breaking API changes, there is a step in our ADO pipeline that will error out if -what was checked in has breaking changes compared to the yaml in main branch. -This script lives in ./build/common/scripts/CheckForBreakingAPISwaggerChanges.ps1 - -You can run this script locally as well: - -``` -.\build\common\scripts\CheckForBreakingAPISwaggerChanges.ps1 -SwaggerDir 'swagger' -Version 'v1-prerelease','v1' -``` - -Example output with no changes: - -``` - -Mode LastWriteTime Length Name ----- ------------- ------ ---- -d----- 8/10/2022 9:22 AM FromMain -Running comparison with baseline for version v1-prerelease -old: swagger\FromMain\v1-prerelease.yaml -new: swagger\v1-prerelease\swagger.yaml -No differences. Specifications are equivalents -Running comparison with baseline for version v1 -old: swagger\FromMain\v1.yaml -new: swagger\v1\swagger.yaml -No differences. Specifications are equivalents - - -PS C:\dev\hls\dicom-server> -``` diff --git a/docs/resources/use-dicom-web-standard-apis-with-python.ipynb b/docs/resources/use-dicom-web-standard-apis-with-python.ipynb deleted file mode 100644 index 425d9985ab..0000000000 --- a/docs/resources/use-dicom-web-standard-apis-with-python.ipynb +++ /dev/null @@ -1,890 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Use DICOMweb™ Standard APIs with Python" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This tutorial uses Python to demonstrate working with the Medical Imaging Server for DICOM.\n", - "\n", - "For the tutorial we will use the DICOM files here: [Sample DICOM files](../dcms). The file name, studyUID, seriesUID and instanceUID of the sample DICOM files is as follows:\n", - "\n", - "| File | StudyUID | SeriesUID | InstanceUID |\n", - "| --- | --- | --- | ---|\n", - "|green-square.dcm|1.2.826.0.1.3680043.8.498.13230779778012324449356534479549187420|1.2.826.0.1.3680043.8.498.45787841905473114233124723359129632652|1.2.826.0.1.3680043.8.498.12714725698140337137334606354172323212|\n", - "|red-triangle.dcm|1.2.826.0.1.3680043.8.498.13230779778012324449356534479549187420|1.2.826.0.1.3680043.8.498.45787841905473114233124723359129632652|1.2.826.0.1.3680043.8.498.47359123102728459884412887463296905395|\n", - "|blue-circle.dcm|1.2.826.0.1.3680043.8.498.13230779778012324449356534479549187420|1.2.826.0.1.3680043.8.498.77033797676425927098669402985243398207|1.2.826.0.1.3680043.8.498.13273713909719068980354078852867170114|\n", - "\n", - "> NOTE: Each of these files represent a single instance and are part of the same study. Also green-square and red-triangle are part of the same series, while blue-circle is in a separate series.\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Prerequisites\n", - "\n", - "In order to use the DICOMWeb™ Standard APIs, you must have an instance of the Medical Imaging Server for DICOM deployed. If you have not already deployed the Medical Imaging Server, [Deploy the Medical Imaging Server to Azure](../quickstarts/deploy-via-azure.md).\n", - "\n", - "Once you have deployed an instance of the Medical Imaging Server for DICOM, retrieve the URL for your App Service:\n", - "\n", - "1. Sign into the [Azure Portal](https://portal.azure.com/).\n", - "1. Search for **App Services** and select your Medical Imaging Server for DICOM App Service.\n", - "1. Copy the **URL** of your App Service.\n", - "\n", - "For this code, we'll be accessing an unsecured dev/test service. Please don't upload any private health information (PHI).\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Working with the Medical Imaging Server for DICOM \n", - "The DICOMweb™ standard makes heavy use of `multipart/related` HTTP requests combined with DICOM specific accept headers. Developers familiar with other REST-based APIs often find working with the DICOMweb™ standard awkward. However, once you have it up and running, it's easy to use. It just takes a little finagling to get started." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Import the appropriate Python libraries\n", - "\n", - "First, import the necessary Python libraries. \n", - "\n", - "We've chosen to implement this example using the synchronous `requests` library. For asnychronous support, consider using `httpx` or another async library. Additionally, we're importing two supporting functions from `urllib3` to support working with `multipart/related` requests." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import requests\n", - "import pydicom\n", - "from pathlib import Path\n", - "from urllib3.filepost import encode_multipart_formdata, choose_boundary" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Configure user-defined variables to be used throughout\n", - "Replace all variable values wrapped in { } with your own values. Additionally, validate that any constructed variables are correct. For instance, `base_url` is constructed using the default URL for Azure App Service. If you're using a custom URL, you'll need to override that value with your own." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "dicom_server_name = \"{server-name}\"\n", - "path_to_dicoms_dir = \"{path to the folder that includes green-square.dcm and other dcm files}\"\n", - "version = \"{version of REST API}\"\n", - "\n", - "base_url = f\"https://{dicom_server_name}.azurewebsites.net/v{version}\"\n", - "base_url" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "dicom_server_name = \"sjbdicomtest\"\n", - "path_to_dicoms_dir = \"c:\\\\githealth\\\\dicom-server\\\\docs\\\\dcms\\\\\"\n", - "version = \"1\"\n", - "\n", - "base_url = f\"https://{dicom_server_name}.azurewebsites.net/v{version}\"\n", - "base_url" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "study_uid = \"1.2.826.0.1.3680043.8.498.13230779778012324449356534479549187420\"; #StudyInstanceUID for all 3 examples\n", - "series_uid = \"1.2.826.0.1.3680043.8.498.45787841905473114233124723359129632652\"; #SeriesInstanceUID for green-square and red-triangle\n", - "instance_uid = \"1.2.826.0.1.3680043.8.498.47359123102728459884412887463296905395\"; #SOPInstanceUID for red-triangle" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Create supporting methods to support `multipart\\related`\n", - "The `Requests` library (and most Python libraries) do not work with `multipart\\related` in a way that supports DICOMweb™. Because of this, we need to add a few methods to support working with DICOM files.\n", - "\n", - "`encode_multipart_related` takes a set of fields (in the DICOM case, these are generally Part 10 dcm files) and an optional user defined boundary. It returns both the full body, along with the content_type, which can be used \n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def encode_multipart_related(fields, boundary=None):\n", - " if boundary is None:\n", - " boundary = choose_boundary()\n", - "\n", - " body, _ = encode_multipart_formdata(fields, boundary)\n", - " content_type = str('multipart/related; boundary=%s' % boundary)\n", - "\n", - " return body, content_type" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Create a `requests` session\n", - "Create a `requests` session, called `client`, that will be used to communicate with the Medical Imaging Server for DICOM." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "client = requests.session()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Store DICOM Instances (STOW)\n", - "\n", - "The following examples highlight persisting DICOM files." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Store-instances-using-multipart/related\n", - "\n", - "This demonstrates how to upload a single DICOM file. This uses a bit of a Python hack to pre-load the DICOM file (as bytes) into memory. By passing an array of files to the fields parameter ofencode_multipart_related, multiple files can be uploaded in a single POST. This is sometimes used to upload a complete Series or Study.\n", - "\n", - "_Details:_\n", - "\n", - "* Path: ../studies\n", - "* Method: POST\n", - "* Headers:\n", - " * `Accept: application/dicom+json`\n", - " * `Content-Type: multipart/related; type=\"application/dicom\"`\n", - "* Body:\n", - " * `Content-Type: application/dicom` for each file uploaded, separated by a boundary value\n", - "\n", - "> Some programming languages and tools behave differently. For instance, some require you to define your own boundary. For those, you may need to use a slightly modified Content-Type header. The following have been used successfully.\n", - " > * `Content-Type: multipart/related; type=\"application/dicom\"; boundary=ABCD1234`\n", - " > * `Content-Type: multipart/related; boundary=ABCD1234`\n", - " > * `Content-Type: multipart/related`\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#upload blue-circle.dcm\n", - "filepath = Path(path_to_dicoms_dir).joinpath('blue-circle.dcm')\n", - "\n", - "# Hack. Need to open up and read through file and load bytes into memory \n", - "with open(filepath,'rb') as reader:\n", - " rawfile = reader.read()\n", - "files = {'file': ('dicomfile', rawfile, 'application/dicom')}\n", - "\n", - "#encode as multipart_related\n", - "body, content_type = encode_multipart_related(fields = files)\n", - "\n", - "headers = {'Accept':'application/dicom+json', \"Content-Type\":content_type}\n", - "\n", - "url = f'{base_url}/studies'\n", - "response = client.post(url, body, headers=headers, verify=False)\n", - "response" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Store-instances-for-a-specific-study\n", - "\n", - "This demonstrates how to upload a multiple DICOM files into the specified study. This uses a bit of a Python hack to pre-load the DICOM file (as bytes) into memory. \n", - "\n", - "By passing an array of files to the fields parameter of `encode_multipart_related`, multiple files can be uploaded in a single POST. This is sometimes used to upload a complete Series or Study. \n", - "\n", - "_Details:_\n", - "* Path: ../studies/{study}\n", - "* Method: POST\n", - "* Headers:\n", - " * `Accept: application/dicom+json`\n", - " * `Content-Type: multipart/related; type=\"application/dicom\"`\n", - "* Body:\n", - " * `Content-Type: application/dicom` for each file uploaded, separated by a boundary value\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "\n", - "filepath_red = Path(path_to_dicoms_dir).joinpath('red-triangle.dcm')\n", - "filepath_green = Path(path_to_dicoms_dir).joinpath('green-square.dcm')\n", - "\n", - "# Hack. Need to open up and read through file and load bytes into memory \n", - "with open(filepath_red,'rb') as reader:\n", - " rawfile_red = reader.read()\n", - "with open(filepath_green,'rb') as reader:\n", - " rawfile_green = reader.read() \n", - " \n", - "files = {'file_red': ('dicomfile', rawfile_red, 'application/dicom'),\n", - " 'file_green': ('dicomfile', rawfile_green, 'application/dicom')}\n", - "\n", - "#encode as multipart_related\n", - "body, content_type = encode_multipart_related(fields = files)\n", - "\n", - "headers = {'Accept':'application/dicom+json', \"Content-Type\":content_type}\n", - "\n", - "url = f'{base_url}/studies'\n", - "response = client.post(url, body, headers=headers, verify=False)\n", - "response\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Store single instance (non-standard)\n", - "\n", - "This demonstrates how to upload a single DICOM file. This non-standard API endpoint simplifies uploading a single file as a byte array stored in the body of a request.\n", - "\n", - "_Details:_\n", - "* Path: ../studies\n", - "* Method: POST\n", - "* Headers:\n", - " * `Accept: application/dicom+json`\n", - " * `Content-Type: application/dicom`\n", - "* Body:\n", - " * Contains a single DICOM file as binary bytes." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#upload blue-circle.dcm\n", - "filepath = Path(path_to_dicoms_dir).joinpath('blue-circle.dcm')\n", - "\n", - "# Hack. Need to open up and read through file and load bytes into memory \n", - "with open(filepath,'rb') as reader:\n", - " body = reader.read()\n", - "\n", - "headers = {'Accept':'application/dicom+json', 'Content-Type':'application/dicom'}\n", - "\n", - "url = f'{base_url}/studies'\n", - "response = client.post(url, body, headers=headers, verify=False)\n", - "response # response should be a 409 Conflict if the file was already uploaded abovin the above request" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Retrieve DICOM Instances (WADO)\n", - "\n", - "The following examples highlight retrieving DICOM instances." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Retrieve all instances within a study\n", - "\n", - "This retrieves all instances within a single study.\n", - "\n", - "_Details:_\n", - "* Path: ../studies/{study}\n", - "* Method: GET\n", - "* Headers:\n", - " * `Accept: multipart/related; type=\"application/dicom\"; transfer-syntax=*`\n", - "\n", - "All three of the dcm files that we uploaded previously are part of the same study so the response should return all 3 instances. Validate that the response has a status code of OK and that all three instances are returned.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "url = f'{base_url}/studies/{study_uid}'\n", - "headers = {'Accept':'multipart/related; type=\"application/dicom\"; transfer-syntax=*'}\n", - "\n", - "response = client.get(url, headers=headers) #, verify=False)\n", - "response\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Use the retrieved instances\n", - "The instances are retrieved as binary bytes. You can loop through the returned items and convert the bytes into a file-like structure which can be read by `pydicom`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import requests_toolbelt as tb\n", - "from io import BytesIO\n", - "\n", - "mpd = tb.MultipartDecoder.from_response(response)\n", - "for part in mpd.parts:\n", - " # Note that the headers are returned as binary!\n", - " print(part.headers[b'content-type'])\n", - " \n", - " # You can convert the binary body (of each part) into a pydicom DataSet\n", - " # And get direct access to the various underlying fields\n", - " dcm = pydicom.dcmread(BytesIO(part.content))\n", - " print(dcm.PatientName)\n", - " print(dcm.SOPInstanceUID)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Retrieve metadata of all instances in study\n", - "\n", - "This request retrieves the metadata for all instances within a single study.\n", - "\n", - "_Details:_\n", - "* Path: ../studies/{study}/metadata\n", - "* Method: GET\n", - "* Headers:\n", - " * `Accept: application/dicom+json`\n", - "\n", - "All three of the dcm files that we uploaded previously are part of the same study so the response should return the metadata for all 3 instances. Validate that the response has a status code of OK and that all the metadata is returned." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "url = f'{base_url}/studies/{study_uid}/metadata'\n", - "headers = {'Accept':'application/dicom+json'}\n", - "\n", - "response = client.get(url, headers=headers) #, verify=False)\n", - "print(response)\n", - "response.json()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Retrieve all instances within a series\n", - "\n", - "This retrieves all instances within a single series.\n", - "\n", - "_Details:_\n", - "* Path: ../studies/{study}/series/{series}\n", - "* Method: GET\n", - "* Headers:\n", - " * `Accept: multipart/related; type=\"application/dicom\"; transfer-syntax=*`\n", - "\n", - "This series has 2 instances (green-square and red-triangle), so the response should return both instances. Validate that the response has a status code of OK and that both instances are returned.\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "url = f'{base_url}/studies/{study_uid}/series/{series_uid}'\n", - "headers = {'Accept':'multipart/related; type=\"application/dicom\"; transfer-syntax=*'}\n", - "\n", - "response = client.get(url, headers=headers) #, verify=False)\n", - "response\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Retrieve metadata of all instances in series\n", - "\n", - "This request retrieves the metadata for all instances within a single series.\n", - "\n", - "_Details:_\n", - "* Path: ../studies/{study}/series/{series}/metadata\n", - "* Method: GET\n", - "* Headers:\n", - " * `Accept: application/dicom+json`\n", - "\n", - "This series has 2 instances (green-square and red-triangle), so the response should return metatdata for both instances. Validate that the response has a status code of OK and that both instances metadata are returned.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "url = f'{base_url}/studies/{study_uid}/series/{series_uid}/metadata'\n", - "headers = {'Accept':'application/dicom+json'}\n", - "\n", - "response = client.get(url, headers=headers) #, verify=False)\n", - "print(response)\n", - "response.json()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Retrieve a single instance within a series of a study\n", - "\n", - "This request retrieves a single instance.\n", - "\n", - "_Details:_\n", - "* Path: ../studies/{study}/series{series}/instances/{instance}\n", - "* Method: GET\n", - "* Headers:\n", - " * `Accept: application/dicom; transfer-syntax=*`\n", - "\n", - "This should only return the instance red-triangle. Validate that the response has a status code of OK and that the instance is returned." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "url = f'{base_url}/studies/{study_uid}/series/{series_uid}/instances/{instance_uid}'\n", - "headers = {'Accept':'application/dicom; transfer-syntax=*'}\n", - "\n", - "response = client.get(url, headers=headers) #, verify=False)\n", - "response\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Retrieve metadata of a single instance within a series of a study\n", - "\n", - "This request retrieves the metadata for a single instances within a single study and series.\n", - "\n", - "_Details:_\n", - "* Path: ../studies/{study}/series{series}/instances/{instance}\n", - "* Method: GET\n", - "* Headers:\n", - " * `Accept: application/dicom; transfer-syntax=*`\n", - "\n", - "This should only return the metatdata for the instance red-triangle. Validate that the response has a status code of OK and that the metadata is returned.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "url = f'{base_url}/studies/{study_uid}/series/{series_uid}/instances/{instance_uid}/metadata'\n", - "headers = {'Accept':'application/dicom+json'}\n", - "\n", - "response = client.get(url, headers=headers) #, verify=False)\n", - "print(response)\n", - "response.json()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Retrieve one or more frames from a single instance\n", - "\n", - "This request retrieves one or more frames from a single instance.\n", - "\n", - "_Details:_\n", - "* Path: ../studies/{study}/series{series}/instances/{instance}/frames/1,2,3\n", - "* Method: GET\n", - "* Headers:\n", - " * `Accept: multipart/related; type=\"application/octet-stream\"; transfer-syntax=1.2.840.10008.1.2.1` (Default) or\n", - " * `Accept: multipart/related; type=\"application/octet-stream\"; transfer-syntax=*` or\n", - " * `Accept: multipart/related; type=\"application/octet-stream\";`\n", - "\n", - "This should return the only frame from the red-triangle. Validate that the response has a status code of OK and that the frame is returned." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "url = f'{base_url}/studies/{study_uid}/series/{series_uid}/instances/{instance_uid}/frames/1'\n", - "headers = {'Accept':'multipart/related; type=\"application/octet-stream\"; transfer-syntax=*'}\n", - "\n", - "response = client.get(url, headers=headers) #, verify=False)\n", - "response\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Query DICOM (QIDO)\n", - "\n", - "In the following examples, we search for items using their unique identifiers. You can also search for other attributes, such as PatientName and the like.\n", - "\n", - "> NOTE: Please see the [Conformance Statement](../resources/conformance-statement.md#supported-search-parameters) file for supported DICOM attributes." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Search for studies\n", - "\n", - "This request searches for one or more studies by DICOM attributes.\n", - "\n", - "_Details:_\n", - "* Path: ../studies?StudyInstanceUID={study}\n", - "* Method: GET\n", - "* Headers:\n", - " * `Accept: application/dicom+json`\n", - "\n", - "Validate that response includes 1 study and that response code is OK." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "url = f'{base_url}/studies'\n", - "headers = {'Accept':'application/dicom+json'}\n", - "params = {'StudyInstanceUID':study_uid}\n", - "\n", - "response = client.get(url, headers=headers, params=params) #, verify=False)\n", - "response" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Search for series\n", - "\n", - "This request searches for one or more series by DICOM attributes.\n", - "\n", - "_Details:_\n", - "* Path: ../series?SeriesInstanceUID={series}\n", - "* Method: GET\n", - "* Headers:\n", - " * `Accept: application/dicom+json`\n", - "\n", - "Validate that response includes 1 series and that response code is OK." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "url = f'{base_url}/series'\n", - "headers = {'Accept':'application/dicom+json'}\n", - "params = {'SeriesInstanceUID':series_uid}\n", - "\n", - "response = client.get(url, headers=headers, params=params) #, verify=False)\n", - "response" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Search for series within a study\n", - "\n", - "This request searches for one or more series within a single study by DICOM attributes.\n", - "\n", - "_Details:_\n", - "* Path: ../studies/{study}/series?SeriesInstanceUID={series}\n", - "* Method: GET\n", - "* Headers:\n", - " * `Accept: application/dicom+json`\n", - "\n", - "Validate that response includes 1 series and that response code is OK.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "url = f'{base_url}/studies/{study_uid}/series'\n", - "headers = {'Accept':'application/dicom+json'}\n", - "params = {'SeriesInstanceUID':series_uid}\n", - "\n", - "response = client.get(url, headers=headers, params=params) #, verify=False)\n", - "response" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Search for instances\n", - "\n", - "This request searches for one or more instances by DICOM attributes.\n", - "\n", - "_Details:_\n", - "* Path: ../instances?SOPInstanceUID={instance}\n", - "* Method: GET\n", - "* Headers:\n", - " * `Accept: application/dicom+json`\n", - "\n", - "Validate that response includes 1 instance and that response code is OK." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "url = f'{base_url}/instances'\n", - "headers = {'Accept':'application/dicom+json'}\n", - "params = {'SOPInstanceUID':instance_uid}\n", - "\n", - "response = client.get(url, headers=headers, params=params) #, verify=False)\n", - "response" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Search for instances within a study\n", - "\n", - "This request searches for one or more instances within a single study by DICOM attributes.\n", - "\n", - "_Details:_\n", - "* Path: ../studies/{study}/instances?SOPInstanceUID={instance}\n", - "* Method: GET\n", - "* Headers:\n", - " * `Accept: application/dicom+json`\n", - "\n", - "Validate that response includes 1 instance and that response code is OK.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "url = f'{base_url}/studies/{study_uid}/instances'\n", - "headers = {'Accept':'application/dicom+json'}\n", - "params = {'SOPInstanceUID':instance_uid}\n", - "\n", - "response = client.get(url, headers=headers, params=params) #, verify=False)\n", - "response" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Search for instances within a study and series\n", - "\n", - "This request searches for one or more instances within a single study and single series by DICOM attributes.\n", - "\n", - "_Details:_\n", - "* Path: ../studies/{study}/series/{series}/instances?SOPInstanceUID={instance}\n", - "* Method: GET\n", - "* Headers:\n", - " * `Accept: application/dicom+json`\n", - "\n", - "Validate that response includes 1 instance and that response code is OK.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "url = f'{base_url}/studies/{study_uid}/series/{series_uid}/instances'\n", - "headers = {'Accept':'application/dicom+json'}\n", - "params = {'SOPInstanceUID':instance_uid}\n", - "\n", - "response = client.get(url, headers=headers, params=params) #, verify=False)\n", - "response" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Delete DICOM\n", - "\n", - "> NOTE: Delete is not part of the DICOM standard, but has been added for convenience.\n", - "\n", - "A 204 response code is returned when the deletion is successful. A 404 response code is returned if the item(s) have never existed or have already been deleted. " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Delete a specific instance within a study and series\n", - "\n", - "This request deletes a single instance within a single study and single series.\n", - "\n", - "_Details:_\n", - "* Path: ../studies/{study}/series/{series}/instances/{instance}\n", - "* Method: DELETE\n", - "* Headers: No special headers needed\n", - "\n", - "This deletes the red-triangle instance from the server. If it is successful the response status code contains no content." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#headers = {'Accept':'anything/at+all'}\n", - "url = f'{base_url}/studies/{study_uid}/series/{series_uid}/instances/{instance_uid}'\n", - "response = client.delete(url) \n", - "response" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Delete a specific series within a study\n", - "\n", - "This request deletes a single series (and all child instances) within a single study.\n", - "\n", - "_Details:_\n", - "* Path: ../studies/{study}/series/{series}\n", - "* Method: DELETE\n", - "* Headers: No special headers needed\n", - "\n", - "\n", - "This deletes the green-square instance (it is the only element left in the series) from the server. If it is successful the response status code contains no content." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#headers = {'Accept':'anything/at+all'}\n", - "url = f'{base_url}/studies/{study_uid}/series/{series_uid}'\n", - "response = client.delete(url) \n", - "response" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Delete a specific study\n", - "\n", - "This request deletes a single study (and all child series and instances).\n", - "\n", - "_Details:_\n", - "* Path: ../studies/{study}\n", - "* Method: DELETE\n", - "* Headers: No special headers needed\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#headers = {'Accept':'anything/at+all'}\n", - "url = f'{base_url}/studies/{study_uid}'\n", - "response = client.delete(url) \n", - "response" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.7" - }, - "toc": { - "base_numbering": 1, - "nav_menu": {}, - "number_sections": true, - "sideBar": true, - "skip_h1_title": false, - "title_cell": "Table of Contents", - "title_sidebar": "Contents", - "toc_cell": false, - "toc_position": {}, - "toc_section_display": true, - "toc_window_display": false - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/docs/resources/v2-conformance-statement.md b/docs/resources/v2-conformance-statement.md deleted file mode 100644 index 3528ebc315..0000000000 --- a/docs/resources/v2-conformance-statement.md +++ /dev/null @@ -1,936 +0,0 @@ -# Version 2 DICOM Conformance Statement - -> API version 2 is the latest API version. - -The **Medical Imaging Server for DICOM** supports a subset of the DICOMweb™ Standard. Support includes: - -- [Studies Service](#studies-service) - - [Store (STOW-RS)](#store-stow-rs) - - [Retrieve (WADO-RS)](#retrieve-wado-rs) - - [Search (QIDO-RS)](#search-qido-rs) - - [Delete (Non-standard)](#delete) -- [Worklist Service (UPS Push and Pull SOPs)](#worklist-service-ups-rs) - - [Create Workitem](#create-workitem) - - [Retrieve Workitem](#retrieve-workitem) - - [Update Workitem](#update-workitem) - - [Change Workitem State](#change-workitem-state) - - [Request Cancellation](#request-cancellation) - - [Search Workitems](#search-workitems) - -Additionally, the following non-standard API(s) are supported: -- [Change Feed](../concepts/change-feed.md) -- [Extended Query Tags](../concepts/extended-query-tags.md) -- [Bulk update](../concepts/bulk-update.md) - -All paths below include an implicit base URL of the server, such as `https://localhost:63838` when running locally. - -The service makes use of REST Api versioning. Do note that the version of the REST API must be explicitly specified as part of the base URL as in the following example: - -`https://localhost:63838/v1/studies` - -For more information on how to specify the version when making requests, visit the [Api Versioning Documentation](../api-versioning.md). - -You can find example requests for supported transactions in the [Postman collection](../resources/Conformance-as-Postman.postman_collection.json). - -## Preamble Sanitization - -The service ignores the 128-byte File Preamble, and replaces its contents with null characters. This ensures that no files passed through the service are -vulnerable to the [malicious preamble vulnerability](https://dicom.nema.org/medical/dicom/current/output/chtml/part10/sect_7.5.html). However, this also means -that [preambles used to encode dual format content](https://www.ncbi.nlm.nih.gov/pmc/articles/PMC6489422/) such as TIFF cannot be used with the service. - -# Studies Service - -The [Studies Service](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#chapter_10) allows users to store, retrieve, and search for DICOM Studies, Series, and Instances. We have added the non-standard Delete transaction to enable a full resource lifecycle. - -## Store (STOW-RS) - -This transaction uses the POST method to Store representations of Studies, Series, and Instances contained in the request payload. - -| Method | Path | Description | -| :----- | :----------------- | :---------- | -| POST | ../studies | Store instances. | -| POST | ../studies/{study} | Store instances for a specific study. | - -Parameter `study` corresponds to the DICOM attribute StudyInstanceUID. If specified, any instance that does not belong to the provided study will be rejected with `43265` warning code. - -The following `Accept` header(s) for the response are supported: - -- `application/dicom+json` - -The following `Content-Type` header(s) are supported: - -- `multipart/related; type="application/dicom"` -- `application/dicom` - -> Note: the Server will not coerce or replace data in the DICOM PS 3.10 file. The DICOM file will be stored as provided, except where otherwise noted. - -### Store Required Attributes -The following DICOM elements are required to be present in every DICOM file attempting to be stored: - -- StudyInstanceUID -- SeriesInstanceUID -- SOPInstanceUID -- SOPClassUID -- PatientID - -> Note: All identifiers must be between 1 and 64 characters long, and only contain alpha numeric characters or the following special characters: `.`, `-`. PatientID continues to be a required tag and can have the value as null in the input. PatientID is validated based on its LO VR type. - -Each file stored must have a unique combination of StudyInstanceUID, SeriesInstanceUID and SopInstanceUID. The warning code `45070` will be returned if a file with the same identifiers already exists. - -> Requests are limited to 2GB. No single DICOM file or combination of files may exceed this limit. - -### Store Changes From V1 -In previous versions, a Store request would fail if any of the [required](#store-required-attributes) or [searchable attributes](#searchable-attributes) failed validation. Beginning with V2, the request will only fail if **required attributes** fail validation. - -Failed validation of attributes not required by the API will still result in the file being stored and a warning will be given about each failing attribute per instance. -When a sequence contains an attribute that fails validation, or when there are multiple issues with a single attribute, only the first failing attribute reason will be noted. - -If an attribute is padded with nulls, the attribute will be indexed when searchable and will be stored as is in dicom+json metadata. No validation warning will be provided. - -### Store Response Status Codes - -| Code | Description | -| :--------------------------- |:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| 200 (OK) | All the SOP instances in the request have been stored. | -| 202 (Accepted) | The origin server stored some of the Instances and others have failed or returned warnings. Additional information regarding this error may be found in the response message body. | -| 204 (No Content) | No content was provided in the store transaction request. | -| 400 (Bad Request) | The request was badly formatted. For example, the provided study instance identifier did not conform the expected UID format. | -| 401 (Unauthorized) | The client is not authenticated. | -| 406 (Not Acceptable) | The specified `Accept` header is not supported. | -| 409 (Conflict) | None of the instances in the store transaction request have been stored. | -| 415 (Unsupported Media Type) | The provided `Content-Type` is not supported. | -| 503 (Service Unavailable) | The service is unavailable or busy. Please try again later. | - -### Store Response Payload - -The response payload will populate a DICOM dataset with the following elements: - -| Tag | Name | Description | -| :----------- | :-------------------- | :---------- | -| (0008, 1190) | RetrieveURL | The Retrieve URL of the study if the StudyInstanceUID was provided in the store request and at least one instance is successfully stored. | -| (0008, 1198) | FailedSOPSequence | The sequence of instances that failed to store. | -| (0008, 1199) | ReferencedSOPSequence | The sequence of stored instances. | - -Each dataset in the `FailedSOPSequence` will have the following elements (if the DICOM file attempting to be stored could be read): - -| Tag | Name | Description | -|:-------------|:-------------------------|:-----------------------------------------------------------------------------------| -| (0008, 1150) | ReferencedSOPClassUID | The SOP class unique identifier of the instance that failed to store. | -| (0008, 1155) | ReferencedSOPInstanceUID | The SOP instance unique identifier of the instance that failed to store. | -| (0008, 1197) | FailureReason | The reason code why this instance failed to store. | -| (0008, 1196) | WarningReason | The reason code why this instance successfully to store, but may have issues. | -| (0074, 1048) | FailedAttributesSequence | The sequence of `ErrorComment` that includes the reason for each failed attribute. | - -Each dataset in the `ReferencedSOPSequence` will have the following elements: - -| Tag | Name | Description | -| :----------- | :----------------------- | :---------- | -| (0008, 1150) | ReferencedSOPClassUID | The SOP class unique identifier of the instance that was stored. | -| (0008, 1155) | ReferencedSOPInstanceUID | The SOP instance unique identifier of the instance that was stored. | -| (0008, 1190) | RetrieveURL | The retrieve URL of this instance on the DICOM server. | - -An example response with `Accept` header `application/dicom+json` without a FailedAttributesSequence in a ReferencedSOPSequence: - -```json -{ - "00081190": - { - "vr":"UR", - "Value":["http://localhost/studies/d09e8215-e1e1-4c7a-8496-b4f6641ed232"] - }, - "00081198": - { - "vr":"SQ", - "Value": - [{ - "00081150": - { - "vr":"UI","Value":["cd70f89a-05bc-4dab-b6b8-1f3d2fcafeec"] - }, - "00081155": - { - "vr":"UI", - "Value":["22c35d16-11ce-43fa-8f86-90ceed6cf4e7"] - }, - "00081197": - { - "vr":"US", - "Value":[43265] - } - }] - }, - "00081199": - { - "vr":"SQ", - "Value": - [{ - "00081150": - { - "vr":"UI", - "Value":["d246deb5-18c8-4336-a591-aeb6f8596664"] - }, - "00081155": - { - "vr":"UI", - "Value":["4a858cbb-a71f-4c01-b9b5-85f88b031365"] - }, - "00081190": - { - "vr":"UR", - "Value":["http://localhost/studies/d09e8215-e1e1-4c7a-8496-b4f6641ed232/series/8c4915f5-cc54-4e50-aa1f-9b06f6e58485/instances/4a858cbb-a71f-4c01-b9b5-85f88b031365"] - } - }] - } -} -``` - -An example response with `Accept` header `application/dicom+json` with a FailedAttributesSequence in a ReferencedSOPSequence: - -```json -{ - "00081190": - { - "vr":"UR", - "Value":["http://localhost/studies/d09e8215-e1e1-4c7a-8496-b4f6641ed232"] - }, - "00081199": - { - "vr":"SQ", - "Value": - [{ - "00081150": - { - "vr":"UI", - "Value":["d246deb5-18c8-4336-a591-aeb6f8596664"] - }, - "00081155": - { - "vr":"UI", - "Value":["4a858cbb-a71f-4c01-b9b5-85f88b031365"] - }, - "00081190": - { - "vr":"UR", - "Value":["http://localhost/studies/d09e8215-e1e1-4c7a-8496-b4f6641ed232/series/8c4915f5-cc54-4e50-aa1f-9b06f6e58485/instances/4a858cbb-a71f-4c01-b9b5-85f88b031365"] - }, - "00081196": { - "vr": "US", - "Value": [ - 1 - ] - }, - "00741048": { - "vr": "SQ", - "Value": [ - { - "00000902": { - "vr": "LO", - "Value": [ - "DICOM100: (0008,0020) - Content \"NotAValidDate\" does not validate VR DA: one of the date values does not match the pattern YYYYMMDD" - ] - } - }, - { - "00000902": { - "vr": "LO", - "Value": [ - "DICOM100: (0008,002a) - Content \"NotAValidDate\" does not validate VR DT: value does not mach pattern YYYY[MM[DD[HH[MM[SS[.F{1-6}]]]]]]" - ] - } - } - ] - } - }] - } -} -``` - -### Store Failure Reason Codes - -| Code | Description | -| :---- | :---------- | -| 272 | The store transaction did not store the instance because of a general failure in processing the operation. | -| 43264 | The DICOM instance failed the validation. | -| 43265 | The provided instance StudyInstanceUID did not match the specified StudyInstanceUID in the store request. | -| 45070 | A DICOM instance with the same StudyInstanceUID, SeriesInstanceUID and SopInstanceUID has already been stored. If you wish to update the contents, delete this instance first. | -| 45071 | A DICOM instance is being created by another process, or the previous attempt to create has failed and the cleanup process has not had chance to clean up yet. Please delete the instance first before attempting to create again. | - -### Store Warning Reason Codes - -| Code | Description | -|:------|:-------------------------------------------------------------------------------------------------------------------------------------------------------| -| 45063 | The Studies Store Transaction (Section 10.5) observed that the Data Set did not match the constraints of the SOP Class during storage of the instance. | -| 1 | The Studies Store Transaction (Section 10.5) observed that the Data Set has validation warnings. | - -### Store Error Codes - -| Code | Description | -| :---- | :---------- | -| 100 | The provided instance attributes did not meet the validation criteria. | - -## Retrieve (WADO-RS) - -This Retrieve Transaction offers support for retrieving stored studies, series, instances and frames by reference. - -| Method | Path | Description | -| :----- | :---------------------------------------------------------------------- | :---------- | -| GET | ../studies/{study} | Retrieves all instances within a study. | -| GET | ../studies/{study}/metadata | Retrieves the metadata for all instances within a study. | -| GET | ../studies/{study}/series/{series} | Retrieves all instances within a series. | -| GET | ../studies/{study}/series/{series}/metadata | Retrieves the metadata for all instances within a series. | -| GET | ../studies/{study}/series/{series}/instances/{instance} | Retrieves a single instance. | -| GET | ../studies/{study}/series/{series}/instances/{instance}/metadata | Retrieves the metadata for a single instance. | -| GET | ../studies/{study}/series/{series}/instances/{instance}/rendered | Retrieves an instance rendered into an image format | -| GET | ../studies/{study}/series/{series}/instances/{instance}/frames/{frames} | Retrieves one or many frames from a single instance. To specify more than one frame, a comma separate each frame to return, e.g. /studies/1/series/2/instance/3/frames/4,5,6 | -| GET | ../studies/{study}/series/{series}/instances/{instance}/frames/{frame}/rendered | Retrieves a single frame rendered into an image format | - -### Retrieve instances within Study or Series - -The following `Accept` header(s) are supported for retrieving instances within a study or a series: - - -- `multipart/related; type="application/dicom"; transfer-syntax=*` -- `multipart/related; type="application/dicom";` (when transfer-syntax is not specified, 1.2.840.10008.1.2.1 is used as default) -- `multipart/related; type="application/dicom"; transfer-syntax=1.2.840.10008.1.2.1` -- `multipart/related; type="application/dicom"; transfer-syntax=1.2.840.10008.1.2.4.90` -- `*/*` (when transfer-syntax is not specified, `*` is used as default and mediaType defaults to `application/dicom`) - -### Retrieve an Instance - -The following `Accept` header(s) are supported for retrieving a specific instance: - -- `application/dicom; transfer-syntax=*` -- `multipart/related; type="application/dicom"; transfer-syntax=*` -- `application/dicom;` (when transfer-syntax is not specified, `1.2.840.10008.1.2.1` is used as default) -- `multipart/related; type="application/dicom"` (when transfer-syntax is not specified, `1.2.840.10008.1.2.1` is used as default) -- `application/dicom; transfer-syntax=1.2.840.10008.1.2.1` -- `multipart/related; type="application/dicom"; transfer-syntax=1.2.840.10008.1.2.1` -- `application/dicom; transfer-syntax=1.2.840.10008.1.2.4.90` -- `multipart/related; type="application/dicom"; transfer-syntax=1.2.840.10008.1.2.4.90` -- `*/*` (when transfer-syntax is not specified, `*` is used as default and mediaType defaults to `application/dicom`) - -### Retrieve Frames - -The following `Accept` headers are supported for retrieving frames: -- `multipart/related; type="application/octet-stream"; transfer-syntax=*` -- `multipart/related; type="application/octet-stream";` (when transfer-syntax is not specified, `1.2.840.10008.1.2.1` is used as default) -- `multipart/related; type="application/octet-stream"; transfer-syntax=1.2.840.10008.1.2.1` -- `multipart/related; type="image/jp2";` (when transfer-syntax is not specified, `1.2.840.10008.1.2.4.90` is used as default) -- `multipart/related; type="image/jp2";transfer-syntax=1.2.840.10008.1.2.4.90` -- `application/octet-stream; transfer-syntax=*` for single frame retrieval -- `*/*` (when transfer-syntax is not specified, `*` is used as default and mediaType defaults to `application/octet-stream`) - -### Retrieve Transfer Syntax - -When the requested transfer syntax is different from original file, the original file is transcoded to requested transfer syntax. The original file needs to be one of below formats for transcoding to succeed, otherwise transcoding may fail: -- 1.2.840.10008.1.2 (Little Endian Implicit) -- 1.2.840.10008.1.2.1 (Little Endian Explicit) -- 1.2.840.10008.1.2.2 (Explicit VR Big Endian) -- 1.2.840.10008.1.2.4.50 (JPEG Baseline Process 1) -- 1.2.840.10008.1.2.4.57 (JPEG Lossless) -- 1.2.840.10008.1.2.4.70 (JPEG Lossless Selection Value 1) -- 1.2.840.10008.1.2.4.90 (JPEG 2000 Lossless Only) -- 1.2.840.10008.1.2.4.91 (JPEG 2000) -- 1.2.840.10008.1.2.5 (RLE Lossless) - -An unsupported `transfer-syntax` will result in `406 Not Acceptable`. - -### Retrieve Metadata (for Study, Series, or Instance) - -The following `Accept` header(s) are supported for retrieving metadata for a study, a series, or an instance: - -- `application/dicom+json` - -Retrieving metadata will not return attributes with the following value representations: - -| VR Name | Description | -| :------ | :--------------------- | -| OB | Other Byte | -| OD | Other Double | -| OF | Other Float | -| OL | Other Long | -| OV | Other 64-Bit Very Long | -| OW | Other Word | -| UN | Unknown | - -Retrieved metadata will include the null character when the attribute was padded with nulls and stored as is. - -### Retrieve Metadata Cache Validation (for Study, Series, or Instance) - -Cache validation is supported using the `ETag` mechanism. In the response of a metadata reqeuest, ETag is returned as one of the headers. This ETag can be cached and added as `If-None-Match` header in the later requests for the same metadata. Two types of responses are possible if the data exists: -- Data has not changed since the last request: HTTP 304 (Not Modified) response will be sent with no body. -- Data has changed since the last request: HTTP 200 (OK) response will be sent with updated ETag. Required data will also be returned as part of the body. - -### Retrieve Rendered Image (For Instance or Frame) -The following `Accept` header(s) are supported for retrieving a rendered image an instance or a frame: - -- `image/jpeg` -- `image/png` - -In the case that no `Accept` header is specified the service will render an `image/jpeg` by default. - -The service only supports rendering of a single frame. If rendering is requested for an instance with multiple frames then only the first frame will be rendered as an image by default. - -When specifying a particular frame to return, frame indexing starts at 1. - -The `quality` query parameter is also supported. An integer value between `1-100` inclusive (1 being worst quality, and 100 being best quality) may be passed as the value for the query paramater. This will only be used for images rendered as `jpeg`, and will be ignored for `png` render requests. If not specified will default to `100`. - -### Retrieve original Image (For Instance and Metadata) - -If you have performed bulk update operation, you can retrieve the original image or metadata by specifying the `msdicom-request-original` header. The value of the header can be `true` or `false`. If the value is `true`, the original image or metadata will be returned. If the value is `false`, the updated image or metadata will be returned. If the value is not specified, the updated image or metadata will be returned. - -> Note: For more information on list of endpoints supported, please refer to [Bulk update retrieve](../concepts/bulk-update.md#retrieve-wado-rs). - -### Retrieve Response Status Codes - -| Code | Description | -| :--------------------------- | :---------- | -| 200 (OK) | All requested data has been retrieved. | -| 304 (Not Modified) | The requested data has not modified since the last request. Content is not added to the response body in such case. Please see [Retrieve Metadata Cache Validation (for Study, Series, or Instance)](###Retrieve-Metadata-Cache-Validation-(for-Study,-Series,-or-Instance)) for more information. | -| 400 (Bad Request) | The request was badly formatted. For example, the provided study instance identifier did not conform the expected UID format or the requested transfer-syntax encoding is not supported. | -| 401 (Unauthorized) | The client is not authenticated. | -| 403 (Forbidden) | The user isn't authorized. | -| 404 (Not Found) | The specified DICOM resource could not be found or for rendered request the instance did not contain pixel data | -| 406 (Not Acceptable) | The specified `Accept` header is not supported or for rendered and transcode requests the file requested was too large | -| 503 (Service Unavailable) | The service is unavailable or busy. Please try again later. | - -## Search (QIDO-RS) - -Query based on ID for DICOM Objects (QIDO) enables you to search for studies, series and instances by attributes. - -| Method | Path | Description | -| :----- | :---------------------------------------------- | :-------------------------------- | -| *Search for Studies* | -| GET | ../studies?... | Search for studies | -| *Search for Series* | -| GET | ../series?... | Search for series | -| GET |../studies/{study}/series?... | Search for series in a study | -| *Search for Instances* | -| GET |../instances?... | Search for instances | -| GET |../studies/{study}/instances?... | Search for instances in a study | -| GET |../studies/{study}/series/{series}/instances?... | Search for instances in a series | - -The following `Accept` header(s) are supported for searching: - -- `application/dicom+json` - -### Search Changes From V1 -If an instance returned validation warnings for [searchable attributes](#searchable-attributes) at the time the [instance was stored](#store-changes-from-v1), those attributes may not be used to search for the stored instance. However, any [searchable attributes](#searchable-attributes) that failed validation will be able to return results if the values are overwritten by instances in the same study/series that are stored after the failed one, or if the values are already stored correctly by a previous instance. If the attribute values are not overwritten, then they will not produce any search results. - -An attribute can be corrected in the following ways: -- Delete the stored instance and upload a new instance with the corrected data -- Upload a new instance in the same study/series with corrected data - -### Supported Search Parameters - -The following parameters for each query are supported: - -| Key | Support Value(s) | Allowed Count | Description | -| :--------------- | :---------------------------- | :------------ | :---------- | -| `{attributeID}=` | {value} | 0...N | Search for attribute/ value matching in query. | -| `includefield=` | `{attributeID}`
`all` | 0...N | The additional attributes to return in the response. Both, public and private tags are supported.
When `all` is provided, please see [Search Response](###Search-Response) for more information about which attributes will be returned for each query type.
If a mixture of {attributeID} and 'all' is provided, the server will default to using 'all'. | -| `limit=` | {value} | 0..1 | Integer value to limit the number of values returned in the response.
Value can be between the range 1 >= x <= 200. Defaulted to 100. | -| `offset=` | {value} | 0..1 | Skip {value} results.
If an offset is provided larger than the number of search query results, a 204 (no content) response will be returned. | -| `fuzzymatching=` | `true` \| `false` | 0..1 | If true fuzzy matching is applied to PatientName attribute. It will do a prefix word match of any name part inside PatientName value. For example, if PatientName is "John^Doe", then "joh", "do", "jo do", "Doe" and "John Doe" will all match. However "ohn" will not match. | - -#### Searchable Attributes - -We support searching on below attributes and search type. - -| Attribute Keyword | All Studies | All Series | All Instances | Study's Series | Study's Instances | Study Series' Instances | -| :---------------- | :---: | :----: | :------: | :---: | :----: | :------: | -| StudyInstanceUID | X | X | X | | | | -| PatientName | X | X | X | | | | -| PatientID | X | X | X | | | | -| PatientBirthDate | X | X | X | | | | -| AccessionNumber | X | X | X | | | | -| ReferringPhysicianName | X | X | X | | | | -| StudyDate | X | X | X | | | | -| StudyDescription | X | X | X | | | | -| ModalitiesInStudy | X | X | X | | | | -| SeriesInstanceUID | | X | X | X | X | | -| Modality | | X | X | X | X | | -| PerformedProcedureStepStartDate | | X | X | X | X | | -| ManufacturerModelName | | X | X | X | X | | -| SOPInstanceUID | | | X | | X | X | - -> Note: We do not support searching using empty string for any attributes. - -#### Search Matching - -We support below matching types. - -| Search Type | Supported Attribute | Example | -| :---------- | :------------------ | :------ | -| Range Query | StudyDate, PatientBirthDate | {attributeID}={value1}-{value2}. For date/ time values, we support an inclusive range on the tag. This will be mapped to `attributeID >= {value1} AND attributeID <= {value2}`. If {value1} is not specified, all occurrences of dates/times prior to and including {value2} will be matched. Likewise, if {value2} is not specified, all occurrences of {value1} and subsequent dates/times will be matched. However, one of these values has to be present. `{attributeID}={value1}-` and `{attributeID}=-{value2}` are valid, however, `{attributeID}=-` is invalid. | -| Exact Match | All supported attributes | {attributeID}={value1} | -| Fuzzy Match | PatientName, ReferringPhysicianName | Matches any component of the name which starts with the value. | - -#### Attribute ID - -Tags can be encoded in a number of ways for the query parameter. We have partially implemented the standard as defined in [PS3.18 6.7.1.1.1](http://dicom.nema.org/medical/dicom/2019a/output/chtml/part18/sect_6.7.html#sect_6.7.1.1.1). The following encodings for a tag are supported: - -| Value | Example | -| :--------------- | :--------------- | -| {group}{element} | 0020000D | -| {dicomKeyword} | StudyInstanceUID | - -Example query searching for instances: -`../instances?Modality=CT&00280011=512&includefield=00280010&limit=5&offset=0` - -### Search Response - -The response will be an array of DICOM datasets. Depending on the resource, by *default* the following attributes are returned: - -#### Default Study tags - -| Tag | Attribute Name | -| :----------- | :------------- | -| (0008, 0020) | StudyDate | -| (0008, 0050) | AccessionNumber | -| (0008, 1030) | StudyDescription | -| (0009, 0090) | ReferringPhysicianName | -| (0010, 0010) | PatientName | -| (0010, 0020) | PatientID | -| (0010, 0030) | PatientBirthDate | -| (0020, 000D) | StudyInstanceUID | - -#### Default Series tags - -| Tag | Attribute Name | -| :----------- | :------------- | -| (0008, 0060) | Modality | -| (0008, 1090) | ManufacturerModelName | -| (0020, 000E) | SeriesInstanceUID | -| (0040, 0244) | PerformedProcedureStepStartDate | - -#### Default Instance tags - -| Tag | Attribute Name | -| :----------- | :------------- | -| (0008, 0018) | SOPInstanceUID | - -If `includefield=all`, below attributes are included along with default attributes. Along with default attributes, this is the full list of attributes supported at each resource level. - -#### Additional Study tags - -| Tag | Attribute Name | -| :----------- | :------------- | -| (0008, 0005) | SpecificCharacterSet | -| (0008, 0030) | StudyTime | -| (0008, 0056) | InstanceAvailability | -| (0008, 0201) | TimezoneOffsetFromUTC | -| (0008, 0063) | AnatomicRegionsInStudyCodeSequence | -| (0008, 1032) | ProcedureCodeSequence | -| (0008, 1060) | NameOfPhysiciansReadingStudy | -| (0008, 1080) | AdmittingDiagnosesDescription | -| (0008, 1110) | ReferencedStudySequence | -| (0010, 1010) | PatientAge | -| (0010, 1020) | PatientSize | -| (0010, 1030) | PatientWeight | -| (0010, 2180) | Occupation | -| (0010, 21B0) | AdditionalPatientHistory | -| (0010, 0040) | PatientSex | -| (0020, 0010) | StudyID | - -#### Additional Series tags - -| Tag | Attribute Name | -| :----------- | :------------- | -| (0008, 0005) | SpecificCharacterSet | -| (0008, 0201) | TimezoneOffsetFromUTC | -| (0020, 0011) | SeriesNumber | -| (0020, 0060) | Laterality | -| (0008, 0021) | SeriesDate | -| (0008, 0031) | SeriesTime | -| (0008, 103E) | SeriesDescription | -| (0040, 0245) | PerformedProcedureStepStartTime | -| (0040, 0275) | RequestAttributesSequence | - -#### Additional Instance tags - -| Tag | Attribute Name | -| :----------- | :------------- | -| (0008, 0005) | SpecificCharacterSet | -| (0008, 0016) | SOPClassUID | -| (0008, 0056) | InstanceAvailability | -| (0008, 0201) | TimezoneOffsetFromUTC | -| (0020, 0013) | InstanceNumber | -| (0028, 0010) | Rows | -| (0028, 0011) | Columns | -| (0028, 0100) | BitsAllocated | -| (0028, 0008) | NumberOfFrames | - -The following attributes are returned: - -- All the match query parameters and UIDs in the resource url. -- `IncludeField` attributes supported at that resource level. -- If the target resource is `All Series`, then `Study` level attributes are also returned. -- If the target resource is `All Instances`, then `Study` and `Series` level attributes are also returned. -- If the target resource is `Study's Instances`, then `Series` level attributes are also returned. -- `NumberOfStudyRelatedInstances` aggregated attribute is supported in `Study` level includeField. -- `NumberOfSeriesRelatedInstances` aggregated attribute is supported in `Series` level includeField. - -### Search Response Codes - -The query API will return one of the following status codes in the response: - -| Code | Description | -| :------------------------ | :---------- | -| 200 (OK) | The response payload contains all the matching resource. | -| 204 (No Content) | The search completed successfully but returned no results. | -| 400 (Bad Request) | The server was unable to perform the query because the query component was invalid. Response body contains details of the failure. | -| 401 (Unauthorized) | The client is not authenticated. | -| 403 (Forbidden) | The user isn't authorized. | -| 503 (Service Unavailable) | The service is unavailable or busy. Please try again later. | - -### Additional Notes - -- Querying using the `TimezoneOffsetFromUTC` (`00080201`) is not supported. -- The query API will not return 413 (request entity too large). If the requested query response limit is outside of the acceptable range, a bad request will be returned. Anything requested within the acceptable range, will be resolved. -- When target resource is Study/Series there is a potential for inconsistent study/series level metadata across multiple instances. For example, two instances could have different patientName. In this case latest will win and you can search only on the latest data. -- Paged results are optimized to return matched *newest* instance first, this may result in duplicate records in subsequent pages if newer data matching the query was added. -- Matching is case in-sensitive and accent in-sensitive for PN VR types. -- Matching is case in-sensitive and accent sensitive for other string VR types. -- Only the first value will be indexed of a single valued data element that incorrectly has multiple values. -- Using the default attributes or limiting the number of results requested will maximize performance. -- When an attribute was stored using null padding, it can be searched for with or without the null padding in uri encoding. Results retrieved will be for attributes stored both with and without null padding. - -## Delete - -This transaction is not part of the official DICOMweb™ Standard. It uses the DELETE method to remove representations of Studies, Series, and Instances from the store. - -| Method | Path | Description | -| :----- | :------------------------------------------------------ | :---------- | -| DELETE | ../studies/{study} | Delete all instances for a specific study. | -| DELETE | ../studies/{study}/series/{series} | Delete all instances for a specific series within a study. | -| DELETE | ../studies/{study}/series/{series}/instances/{instance} | Delete a specific instance within a series. | - -Parameters `study`, `series` and `instance` correspond to the DICOM attributes StudyInstanceUID, SeriesInstanceUID and SopInstanceUID respectively. - -There are no restrictions on the request's `Accept` header, `Content-Type` header or body content. - -> Note: After a Delete transaction the deleted instances will not be recoverable. -> If you have performed bulk update and delete operations, both the versions are deleted and not recoverable. For more information, see [Bulk update](../concepts/bulk-update.md#delete). - -### Response Status Codes - -| Code | Description | -| :--------------------------- | :---------- | -| 204 (No Content) | When all the SOP instances have been deleted. | -| 400 (Bad Request) | The request was badly formatted. | -| 401 (Unauthorized) | The client is not authenticated. | -| 401 (Unauthorized) | The client isn't authenticated. | -| 403 (Forbidden) | The user isn't authorized. | -| 404 (Not Found) | When the specified series was not found within a study, or the specified instance was not found within the series. | -| 503 (Service Unavailable) | The service is unavailable or busy. Please try again later. | - -### Delete Response Payload - -The response body will be empty. The status code is the only useful information returned. - -# Worklist Service (UPS-RS) - -The DICOM service supports the Push and Pull SOPs of the [Worklist Service (UPS-RS)](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#chapter_11). This service provides access to one Worklist containing Workitems, each of which represents a Unified Procedure Step (UPS). - -Throughout, the variable `{workitem}` in a URI template stands for a Workitem UID. - -## Create Workitem - -This transaction uses the POST method to create a new Workitem. - -| Method | Path | Description | -| :----- | :----------------- | :---------- | -| POST | `../workitems` | Create a Workitem. | -| POST | `../workitems?{workitem}` | Creates a Workitem with the specified UID. | - - -If not specified in the URI, the payload dataset must contain the Workitem in the SOPInstanceUID attribute. - -The `Accept` and `Content-Type` headers are required in the request, and must both have the value `application/dicom+json`. - -There are a number of requirements related to DICOM data attributes in the context of a specific transaction. Attributes may be -required to be present, required to not be present, required to be empty, or required to not be empty. These requirements can be -found in [this table](https://dicom.nema.org/medical/dicom/current/output/html/part04.html#table_CC.2.5-3). - -Notes on dataset attributes: -- **SOP Instance UID:** Although the reference table above says that SOP Instance UID should not be present, this guidance is specific to the DIMSE protocol and is -handled diferently in DICOMWeb™. SOP Instance UID **should be present** in the dataset if not in the URI. -- **Conditional requirement codes:** All the conditional requirement codes including 1C and 2C are treated as optional. - -### Create Response Status Codes - -| Code | Description | -| :--------------------------- | :---------- | -| 201 (Created) | The target Workitem was successfully created. | -| 400 (Bad Request) | There was a problem with the request. For example, the request payload did not satisfy the requirements above. | -| 401 (Unauthorized) | The client is not authenticated. | -| 403 (Forbidden) | The user isn't authorized. | -| 409 (Conflict) | The Workitem already exists. | -| 415 (Unsupported Media Type) | The provided `Content-Type` is not supported. | -| 503 (Service Unavailable) | The service is unavailable or busy. Please try again later. | - -### Create Response Payload - -A success response will have no payload. The `Location` and `Content-Location` response headers will contain -a URI reference to the created Workitem. - -A failure response payload will contain a message describing the failure. - -## Request Cancellation - -This transaction enables the user to request cancellation of a non-owned Workitem. - -There are -[four valid Workitem states](https://dicom.nema.org/medical/dicom/current/output/html/part04.html#table_CC.1.1-1): -- `SCHEDULED` -- `IN PROGRESS` -- `CANCELED` -- `COMPLETED` - -This transaction will only succeed against Workitems in the `SCHEDULED` state. Any user can claim ownership of a Workitem by -setting its Transaction UID and changing its state to `IN PROGRESS`. From then on, a user can only modify the Workitem by providing -the correct Transaction UID. While UPS defines Watch and Event SOP classes that allow cancellation requests and other events to be -forwarded, this DICOM service does not implement these classes, and so cancellation requests on workitems that are `IN PROGRESS` will -return failure. An owned Workitem can be canceled via the [Change Workitem State](#change-workitem-state) transaction. - -| Method | Path | Description | -| :------ | :---------------------------------------------- | :----------------------------------------------- | -| POST | ../workitems/{workitem}/cancelrequest | Request the cancellation of a scheduled Workitem | - -The `Content-Type` headers is required, and must have the value `application/dicom+json`. - -The request payload may include Action Information as [defined in the DICOM Standard](https://dicom.nema.org/medical/dicom/current/output/html/part04.html#table_CC.2.2-1). - -### Request Cancellation Response Status Codes - -| Code | Description | -| :--------------------------- | :---------- | -| 202 (Accepted) | The request was accepted by the server, but the Target Workitem state has not necessarily changed yet. | -| 400 (Bad Request) | There was a problem with the syntax of the request. | -| 401 (Unauthorized) | The client is not authenticated. | -| 403 (Forbidden) | The user isn't authorized. | -| 404 (Not Found) | The Target Workitem was not found. | -| 409 (Conflict) | The request is inconsistent with the current state of the Target Workitem. For example, the Target Workitem is in the SCHEDULED or COMPLETED state. | -| 415 (Unsupported Media Type) | The provided `Content-Type` is not supported. | -| 503 (Service Unavailable) | The service is unavailable or busy. Please try again later. | - -### Request Cancellation Response Payload - -A success response will have no payload, and a failure response payload will contain a message describing the failure. -If the Workitem Instance is already in a canceled state, the response will include the following HTTP Warning header: -`299: The UPS is already in the requested state of CANCELED.` - - -## Retrieve Workitem - -This transaction retrieves a Workitem. It corresponds to the UPS DIMSE N-GET operation. - -Refer: https://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_11.5 - -If the Workitem exists on the origin server, the Workitem shall be returned in an Acceptable Media Type. The returned Workitem shall not contain the Transaction UID (0008,1195) Attribute. This is necessary to preserve this Attribute's role as an access lock. - -| Method | Path | Description | -| :------ | :---------------------- | :------------ | -| GET | ../workitems/{workitem} | Request to retrieve a Workitem | - -The `Accept` header is required, and must have the value `application/dicom+json`. - -### Retrieve Workitem Response Status Codes - -| Code | Description | -| :---------------------------- | :---------- | -| 200 (OK) | Workitem Instance was successfully retrieved. | -| 400 (Bad Request) | There was a problem with the request. | -| 401 (Unauthorized) | The client is not authenticated. | -| 403 (Forbidden) | The user isn't authorized. | -| 404 (Not Found) | The Target Workitem was not found. | -| 503 (Service Unavailable) | The service is unavailable or busy. Please try again later. | - -### Retrieve Workitem Response Payload - -* A success response has a single part payload containing the requested Workitem in the Selected Media Type. -* The returned Workitem shall not contain the Transaction UID (0008,1195) Attribute of the Workitem, since that should only be known to the Owner. - -## Update Workitem - -This transaction modifies attributes of an existing Workitem. It corresponds to the UPS DIMSE N-SET operation. - -Refer: https://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_11.6 - -To update a Workitem currently in the SCHEDULED state, the Transaction UID Attribute shall not be present. For a Workitem in the IN PROGRESS state, the request must include the current Transaction UID as a query parameter. If the Workitem is already in the COMPLETED or CANCELED states, the response will be 400 (Bad Request). - -| Method | Path | Description | -| :------ | :------------------------------ | :-------------------- | -| POST | ../workitems/{workitem}?{transaction-uid} | Update Workitem Transaction | - -The `Content-Type` header is required, and must have the value `application/dicom+json`. - -The request payload contains a dataset with the changes to be applied to the target Workitem. When modifying a sequence, the request must include all Items in the sequence, not just the Items to be modified. -When multiple Attributes need updating as a group, do this as multiple Attributes in a single request, not as multiple requests. - -There are a number of requirements related to DICOM data attributes in the context of a specific transaction. Attributes may be -required to be present, required to not be present, required to be empty, or required to not be empty. These requirements can be -found in [this table](https://dicom.nema.org/medical/dicom/current/output/html/part04.html#table_CC.2.5-3). - -Notes on dataset attributes: -- **Conditional requirement codes:** All the conditional requirement codes including 1C and 2C are treated as optional. - -The request cannot set the value of the Procedure Step State (0074,1000) Attribute. Procedure Step State is managed using the Change State transaction, or the Request Cancellation transaction. - -### Update Workitem Transaction Response Status Codes -| Code | Description | -| :---------------------------- | :---------- | -| 200 (OK) | The Target Workitem was updated. | -| 400 (Bad Request) | There was a problem with the request. For example: (1) the Target Workitem was in the COMPLETED or CANCELED state. (2) the Transaction UID is missing. (3) the Transaction UID is incorrect. (4) the dataset did not conform to the requirements. -| 401 (Unauthorized) | The client is not authenticated. | -| 403 (Forbidden) | The user isn't authorized. | -| 404 (Not Found) | The Target Workitem was not found. | -| 409 (Conflict) | The request is inconsistent with the current state of the Target Workitem. | -| 415 (Unsupported Media Type) | The provided `Content-Type` is not supported. | -| 503 (Service Unavailable) | The service is unavailable or busy. Please try again later. | - -### Update Workitem Transaction Response Payload -The origin server shall support header fields as required in [Table 11.6.3-2](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#table_11.6.3-2). - -A success response shall have either no payload, or a payload containing a Status Report document. - -A failure response payload may contain a Status Report describing any failures, warnings, or other useful information. - -## Change Workitem State - -This transaction is used to change the state of a Workitem. It corresponds to the UPS DIMSE N-ACTION operation "Change UPS State". State changes are used to claim ownership, complete, or cancel a Workitem. - -Refer: https://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_11.7 - -If the Workitem exists on the origin server, the Workitem shall be returned in an Acceptable Media Type. The returned Workitem shall not contain the Transaction UID (0008,1195) Attribute. This is necessary to preserve this Attribute's role as an access lock as described [here.](https://dicom.nema.org/medical/dicom/current/output/html/part04.html#sect_CC.1.1) - -| Method | Path | Description | -| :------ | :------------------------------ | :-------------------- | -| PUT | ../workitems/{workitem}/state | Change Workitem State | - -The `Accept` header is required, and must have the value `application/dicom+json`. - -The request payload shall contain the Change UPS State Data Elements. These data elements are: - -* **Transaction UID (0008,1195)** -The request payload shall include a Transaction UID. The user agent creates the Transaction UID when requesting a transition to the IN PROGRESS state for a given Workitem. The user agent provides that Transaction UID in subsequent transactions with that Workitem. - -* **Procedure Step State (0074,1000)** -The legal values correspond to the requested state transition. They are: "IN PROGRESS", "COMPLETED", or "CANCELED". - - -### Change Workitem State Response Status Codes - -| Code | Description | -| :---------------------------- | :---------- | -| 200 (OK) | Workitem Instance was successfully retrieved. | -| 400 (Bad Request) | The request cannot be performed for one of the following reasons: (1) the request is invalid given the current state of the Target Workitem. (2) the Transaction UID is missing. (3) the Transaction UID is incorrect -| 401 (Unauthorized) | The client is not authenticated. | -| 403 (Forbidden) | The user isn't authorized. | -| 404 (Not Found) | The Target Workitem was not found. | -| 409 (Conflict) | The request is inconsistent with the current state of the Target Workitem. | -| 503 (Service Unavailable) | The service is unavailable or busy. Please try again later. | - -### Change Workitem State Response Payload - -* Responses will include the header fields specified in [section 11.7.3.2](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_11.7.3.2) -* A success response shall have no payload. -* A failure response payload may contain a Status Report describing any failures, warnings, or other useful information. - -## Search Workitems - -This transaction enables you to search for Workitems by attributes. - -| Method | Path | Description | -| :----- | :---------------------------------------------- | :-------------------------------- | -| GET | ../workitems? | Search for Workitems | - -The following `Accept` header(s) are supported for searching: - -- `application/dicom+json` - -### Supported Search Parameters - -The following parameters for each query are supported: - -| Key | Support Value(s) | Allowed Count | Description | -| :--------------- | :---------------------------- | :------------ | :---------- | -| `{attributeID}=` | {value} | 0...N | Search for attribute/ value matching in query. | -| `includefield=` | `{attributeID}`
`all` | 0...N | The additional attributes to return in the response. Only top-level attributes can be specified to be included - not attributes that are part of sequences. Both public and private tags are supported.
When `all` is provided, please see [Search Response](###Search-Response) for more information about which attributes will be returned for each query type.
If a mixture of {attributeID} and 'all' is provided, the server will default to using 'all'. | -| `limit=` | {value} | 0...1 | Integer value to limit the number of values returned in the response.
Value can be between the range 1 >= x <= 200. Defaulted to 100. | -| `offset=` | {value} | 0...1 | Skip {value} results.
If an offset is provided larger than the number of search query results, a 204 (no content) response will be returned. | -| `fuzzymatching=` | `true` \| `false` | 0...1 | If true fuzzy matching is applied to any attributes with the Person Name (PN) Value Representation (VR). It will do a prefix word match of any name part inside these attributes. For example, if PatientName is "John^Doe", then "joh", "do", "jo do", "Doe" and "John Doe" will all match. However "ohn" will not match. | - -#### Searchable Attributes - -We support searching on these attributes: - -| Attribute Keyword | -| :---------------- | -| PatientName | -| PatientID | -| ReferencedRequestSequence.AccessionNumber | -| ReferencedRequestSequence.RequestedProcedureID | -| ScheduledProcedureStepStartDateTime | -| ScheduledStationNameCodeSequence.CodeValue | -| ScheduledStationClassCodeSequence.CodeValue | -| ScheduledStationGeographicLocationCodeSequence.CodeValue | -| ProcedureStepState | -| StudyInstanceUID | - -> Note: We do not support searching using empty string for any attributes. - -#### Search Matching - -We support these matching types: - -| Search Type | Supported Attribute | Example | -| :---------- | :------------------ | :------ | -| Range Query | Scheduled​Procedure​Step​Start​Date​Time | {attributeID}={value1}-{value2}. For date/ time values, we support an inclusive range on the tag. This will be mapped to `attributeID >= {value1} AND attributeID <= {value2}`. If {value1} is not specified, all occurrences of dates/times prior to and including {value2} will be matched. Likewise, if {value2} is not specified, all occurrences of {value1} and subsequent dates/times will be matched. However, one of these values has to be present. `{attributeID}={value1}-` and `{attributeID}=-{value2}` are valid, however, `{attributeID}=-` is invalid. | -| Exact Match | All supported attributes | {attributeID}={value1} | -| Fuzzy Match | PatientName | Matches any component of the name which starts with the value. | - -> Note: While we do not support full sequence matching, we do support exact match on the attributes listed above that are contained in a sequence. - -#### Attribute ID - -Tags can be encoded in a number of ways for the query parameter. We have partially implemented the standard as defined in [PS3.18 6.7.1.1.1](http://dicom.nema.org/medical/dicom/2019a/output/chtml/part18/sect_6.7.html#sect_6.7.1.1.1). The following encodings for a tag are supported: - -| Value | Example | -| :--------------- | :--------------- | -| {group}{element} | 00100010 | -| {dicomKeyword} | PatientName | - -Example query: **../workitems?PatientID=K123&0040A370.00080050=1423JS&includefield=00404005&limit=5&offset=0** - -### Search Response - -The response will be an array of 0...N DICOM datasets. The following attributes are returned: - - - All attributes in [DICOM PS 3.4 Table CC.2.5-3](https://dicom.nema.org/medical/dicom/current/output/html/part04.html#table_CC.2.5-3) with a Return Key Type of 1 or 2. - - All attributes in [DICOM PS 3.4 Table CC.2.5-3](https://dicom.nema.org/medical/dicom/current/output/html/part04.html#table_CC.2.5-3) with a Return Key Type of 1C for which the conditional requirements are met. - - All other Workitem attributes passed as match parameters. - - All other Workitem attributes passed as includefield parameter values. - -### Search Response Codes - -The query API will return one of the following status codes in the response: - -| Code | Description | -| :------------------------ | :---------- | -| 200 (OK) | The response payload contains all the matching resource. | -| 206 (Partial Content) | The response payload contains only some of the search results, and the rest can be requested through the appropriate request. | -| 204 (No Content) | The search completed successfully but returned no results. | -| 400 (Bad Request) | The was a problem with the request. For example, invalid Query Parameter syntax. Response body contains details of the failure. | -| 401 (Unauthorized) | The client is not authenticated. | -| 403 (Forbidden) | The user isn't authorized. | -| 503 (Service Unavailable) | The service is unavailable or busy. Please try again later. | - -### Additional Notes - -- The query API will not return 413 (request entity too large). If the requested query response limit is outside of the acceptable range, a bad request will be returned. Anything requested within the acceptable range, will be resolved. -- Paged results are optimized to return matched *newest* instance first, this may result in duplicate records in subsequent pages if newer data matching the query was added. -- Matching is case insensitive and accent insensitive for PN VR types. -- Matching is case insensitive and accent sensitive for other string VR types. -- If there is a scenario where canceling a Workitem and querying the same happens at the same time, then the query will most likely exclude the Workitem that is getting updated and the response code will be 206 (Partial Content). diff --git a/docs/tutorials/use-dicom-web-standard-apis-with-c#.md b/docs/tutorials/use-dicom-web-standard-apis-with-c#.md deleted file mode 100644 index 651bbf9bbe..0000000000 --- a/docs/tutorials/use-dicom-web-standard-apis-with-c#.md +++ /dev/null @@ -1,343 +0,0 @@ -# Use DICOMweb™ Standard APIs with C# - -This tutorial uses C# to demonstrate working with the Medical Imaging Server for DICOM. - -For the tutorial we will use the DICOM files here: [Sample DICOM files](../dcms). The file name, studyUID, seriesUID and instanceUID of the sample DICOM files is as follows: - -| File | StudyUID | SeriesUID | InstanceUID | -| --- | --- | --- | ---| -|green-square.dcm|1.2.826.0.1.3680043.8.498.13230779778012324449356534479549187420|1.2.826.0.1.3680043.8.498.45787841905473114233124723359129632652|1.2.826.0.1.3680043.8.498.12714725698140337137334606354172323212| -|red-triangle.dcm|1.2.826.0.1.3680043.8.498.13230779778012324449356534479549187420|1.2.826.0.1.3680043.8.498.45787841905473114233124723359129632652|1.2.826.0.1.3680043.8.498.47359123102728459884412887463296905395| -|blue-circle.dcm|1.2.826.0.1.3680043.8.498.13230779778012324449356534479549187420|1.2.826.0.1.3680043.8.498.77033797676425927098669402985243398207|1.2.826.0.1.3680043.8.498.13273713909719068980354078852867170114| - -> NOTE: Each of these files represent a single instance and are part of the same study. Also green-square and red-triangle are part of the same series, while blue-circle is in a separate series. - -## Prerequisites - -In order to use the DICOMWeb™ Standard APIs, you must have an instance of the Medical Imaging Server for DICOM deployed. If you have not already deployed the Medical Imaging Server, [Deploy the Medical Imaging Server to Azure](../quickstarts/deploy-via-azure.md). - -Once you have deployed an instance of the Medical Imaging Server for DICOM, retrieve the URL for your App Service: - -1. Sign into the [Azure Portal](https://portal.azure.com/). -1. Search for **App Services** and select your Medical Imaging Server for DICOM App Service. -1. Copy the **URL** of your App Service. -1. Note the version of the REST API you would like to use. When creating the `DicomWebClient` below, we recommend to pass the version in to pin the client to a specific version. For more information on versioning visit the [Api Versioning Documentation](../api-versioning.md). - -In your application install the following nuget packages: - -1. [Dicom Client](https://microsofthealthoss.visualstudio.com/FhirServer/_packaging?_a=package&feed=Public&package=Microsoft.Health.Dicom.Client&protocolType=NuGet) -2. [fo-dicom](https://www.nuget.org/packages/fo-dicom/) - -## Create a `DicomWebClient` - -After you have deployed your Medical Imaging Server for DICOM, you will create a 'DicomWebClient'. Run the following code snippet to create `DicomWebClient` which we will be using for the rest of the tutorial. Ensure you have both nuget packages mentioned above installed. - -```c# -string webServerUrl ="{Your DicomWeb Server URL}" -var httpClient = new HttpClient(); -httpClient.BaseAddress = new Uri(webServerUrl); -IDicomWebClient client = new DicomWebClient(httpClient, "v"); -``` - -With the `DicomWebClient` we can now perform Store, Retrieve, Search, and Delete operations. - -## Store DICOM Instances (STOW) - -Using the `DicomWebClient` that we have created, we can now store DICOM files. - -### Store single instance - -This demonstrates how to upload a single DICOM file. - -_Details:_ - -* POST /studies - -```c# -DicomFile dicomFile = await DicomFile.OpenAsync(@"{Path To blue-circle.dcm}"); -DicomWebResponse response = await client.StoreAsync(new[] { dicomFile }); -``` - -### Store instances for a specific study - -This demonstrates how to upload a DICOM file into a specified study. - -_Details:_ - -* POST /studies/{study} - -```c# -DicomFile dicomFile = await DicomFile.OpenAsync(@"{Path To red-triangle.dcm}"); -DicomWebResponse response = await client.StoreAsync(new[] { dicomFile }, "1.2.826.0.1.3680043.8.498.13230779778012324449356534479549187420"); -``` - -Before moving on to the next part also upload the green-square.dcm file using either of the methods above. - -## Retrieving DICOM instance(s) (WADO) - -The following code snippets will demonstrate how to perform each of the retrieve queries using the `DicomWebClient` created earlier. - -The following variables will be used throghout the rest of the examples: - -```c# -string studyInstanceUid = "1.2.826.0.1.3680043.8.498.13230779778012324449356534479549187420"; //StudyInstanceUID for all 3 examples -string seriesInstanceUid = "1.2.826.0.1.3680043.8.498.45787841905473114233124723359129632652"; //SeriesInstanceUID for green-square and red-triangle -string sopInstanceUid = "1.2.826.0.1.3680043.8.498.47359123102728459884412887463296905395"; //SOPInstanceUID for red-triangle -``` - -### Retrieve all instances within a study - -This retrieves all instances within a single study. - -_Details:_ - -* GET /studies/{study} - -```c# -DicomWebResponse response = await client.RetrieveStudyAsync(studyInstanceUid); -``` - -All three of the dcm files that we uploaded previously are part of the same study so the response should return all 3 instances. Validate that the response has a status code of OK and that all three instances are returned. - -### Use the retrieved instances - -The following code snippet shows how to access the instances that are retrieved, how to access some of the fields of the instances, and how to save it as a .dcm file. - -```c# -DicomWebAsyncEnumerableResponse response = await client.RetrieveStudyAsync(studyInstanceUid); -await foreach (DicomFile file in response) -{ - string patientName = file.Dataset.GetString(DicomTag.PatientName); - string studyId = file.Dataset.GetString(DicomTag.StudyID); - string seriesNumber = file.Dataset.GetString(DicomTag.SeriesNumber); - string instanceNumber = file.Dataset.GetString(DicomTag.InstanceNumber); - - file.Save($"\\{patientName}{studyId}{seriesNumber}{instanceNumber}.dcm"); -} -``` - -### Retrieve metadata of all instances in study - -This request retrieves the metadata for all instances within a single study. - -_Details:_ - -* GET /studies/{study}/metadata - -```c# -DicomWebResponse response = await client.RetrieveStudyMetadataAsync(studyInstanceUid); -``` - -All three of the dcm files that we uploaded previously are part of the same study so the response should return the metadata for all 3 instances. Validate that the response has a status code of OK and that all the metadata is returned. - -### Retrieve all instances within a series - -This request retrieves all instances within a single series. - -_Details:_ - -* GET /studies/{study}/series/{series} - -```c# -DicomWebResponse response = await client.RetrieveSeriesAsync(studyInstanceUid, seriesInstanceUid); -``` - -This series has 2 instances (green-square and red-triangle), so the response should return both instances. Validate that the response has a status code of OK and that both instances are returned. - -### Retrieve metadata of all instances within a series - -This request retrieves the metadata for all instances within a single study. - -_Details:_ - -* GET /studies/{study}/series/{series}/metadata - -```c# -DicomWebResponse response = await client.RetrieveSeriesMetadataAsync(studyInstanceUid, seriesInstanceUid); -``` - -This series has 2 instances (green-square and red-triangle), so the response should return metatdata for both instances. Validate that the response has a status code of OK and that both instances metadata are returned. - -### Retrieve a single instance within a series of a study - -This request retrieves a single instances. - -_Details:_ - -* GET /studies/{study}/series{series}/instances/{instance} - -```c# -DicomWebResponse response = await client.RetrieveInstanceAsync(studyInstanceUid, seriesInstanceUid, sopInstanceUid); -``` - -This should only return the instance red-triangle. Validate that the response has a status code of OK and that the instance is returned. - -### Retrieve metadata of a single instance within a series of a study - -This request retrieves the metadata for a single instances within a single study and series. - -_Details:_ - -* GET /studies/{study}/series/{series}/instances/{instance}/metadata - -```c# -DicomWebResponse response = await client.RetrieveInstanceMetadataAsync(studyInstanceUid, seriesInstanceUid, sopInstanceUid); -``` - -This should only return the metatdata for the instance red-triangle. Validate that the response has a status code of OK and that the metadata is returned. - -### Retrieve one or more frames from a single instance - -This request retrieves one or more frames from a single instance. - -_Details:_ - -* GET /studies/{study}/series/{series}/instances/{instance}/frames/{frames} - -```c# -DicomWebResponse response = await client.RetrieveFramesAsync(studyInstanceUid, seriesInstanceUid, sopInstanceUid, frames: new[] { 1 }); - -``` - -This should return the only frame from the red-triangle. Validate that the response has a status code of OK and that the frame is returned. - -## Query DICOM (QIDO) - -> NOTE: Please see the [Conformance Statement](../resources/conformance-statement.md#supported-search-parameters) file for supported DICOM attributes. - -### Search for studies - -This request searches for one or more studies by DICOM attributes. - -_Details:_ - -* GET /studies?StudyInstanceUID={study} - -```c# -string query = $"/studies?StudyInstanceUID={studyInstanceUid}"; -DicomWebResponse response = await client.QueryAsync(query); -``` - -Validate that response includes 1 study and that response code is OK. - -### Search for series - -This request searches for one or more series by DICOM attributes. - -_Details:_ - -* GET /series?SeriesInstanceUID={series} - -```c# -string query = $"/series?SeriesInstanceUID={seriesInstanceUid}"; -DicomWebResponse response = await client.QueryAsync(query); -``` - -Validate that response includes 1 series and that response code is OK. - -### Search for series within a study - -This request searches for one or more series within a single study by DICOM attributes. - -_Details:_ - -* GET /studies/{study}/series?SeriesInstanceUID={series} - -```c# -string query = $"/studies/{studyInstanceUid}/series?SeriesInstanceUID={seriesInstanceUid}"; -DicomWebResponse response = await client.QueryAsync(query); -``` - -Validate that response includes 1 series and that response code is OK. - -### Search for instances - -This request searches for one or more instances by DICOM attributes. - -_Details:_ - -* GET /instances?SOPInstanceUID={instance} - -```c# -string query = $"/instances?SOPInstanceUID={sopInstanceUid}"; -DicomWebResponse response = await client.QueryAsync(query); -``` - -Validate that response includes 1 instance and that response code is OK. - -### Search for instances within a study - -This request searches for one or more instances within a single study by DICOM attributes. - -_Details:_ - -* GET /studies/{study}/instances?SOPInstanceUID={instance} - -```c# -string query = $"/studies/{studyInstanceUid}/instances?SOPInstanceUID={sopInstanceUid}"; -DicomWebResponse response = await client.QueryAsync(query); -``` - -Validate that response includes 1 instance and that response code is OK. - -### Search for instances within a study and series - -This request searches for one or more instances within a single study and single series by DICOM attributes. - -_Details:_ - -* GET /studies/{study}/series/{series}instances?SOPInstanceUID={instance} - -```c# -string query = $"/studies/{studyInstanceUid}/series/{seriesInstanceUid}/instances?SOPInstanceUID={sopInstanceUid}"; -DicomWebResponse response = await client.QueryAsync(query); -``` - -Validate that response includes 1 instance and that response code is OK. - -## Delete DICOM - -> NOTE: Delete is not part of the DICOM standard, but has been added for convenience. - -### Delete a specific instance within a study and series - -This request deletes a single instance within a single study and single series. - -_Details:_ - -* DELETE /studies/{study}/series/{series}/instances/{instance} - -```c# -string sopInstanceUidRed = "1.2.826.0.1.3680043.8.498.47359123102728459884412887463296905395"; -DicomWebResponse response = await client.DeleteInstanceAsync(studyInstanceUid, seriesInstanceUid, sopInstanceUidRed); -``` - -This deletes the red-triangle instance from the server. If it is successful the response status code contains no content. - -### Delete a specific series within a study - -This request deletes a single series (and all child instances) within a single study. - -_Details:_ - -* DELETE /studies/{study}/series/{series} - -```c# -DicomWebResponse response = await client.DeleteSeriesAsync(studyInstanceUid, seriesInstanceUid); -``` - -This deletes the green-square instance (it is the only element left in the series) from the server. If it is successful the response status code contains no content. - -### Delete a specific study - -This request deletes a single study (and all child series and instances). - -_Details:_ - -* DELETE /studies/{study} - -```c# -DicomWebResponse response = await client.DeleteStudyAsync(studyInstanceUid); -``` - -This deletes the blue-circle instance (it is the only element left in the series) from the server. If it is successful the response status code contains no content. diff --git a/docs/tutorials/use-dicom-web-standard-apis-with-curl.md b/docs/tutorials/use-dicom-web-standard-apis-with-curl.md deleted file mode 100644 index 0a8cd21c01..0000000000 --- a/docs/tutorials/use-dicom-web-standard-apis-with-curl.md +++ /dev/null @@ -1,343 +0,0 @@ -# Use DICOMWeb™ Standard APIs with cURL - -This tutorial uses cURL to demonstrate working with the Medical Imaging Server for DICOM. - -For the tutorial we will use the DICOM files here: [Sample DICOM files](../dcms). The file name, studyUID, seriesUID and instanceUID of the sample DICOM files is as follows: - -| File | StudyUID | SeriesUID | InstanceUID | -| --- | --- | --- | ---| -|green-square.dcm|1.2.826.0.1.3680043.8.498.13230779778012324449356534479549187420|1.2.826.0.1.3680043.8.498.45787841905473114233124723359129632652|1.2.826.0.1.3680043.8.498.12714725698140337137334606354172323212| -|red-triangle.dcm|1.2.826.0.1.3680043.8.498.13230779778012324449356534479549187420|1.2.826.0.1.3680043.8.498.45787841905473114233124723359129632652|1.2.826.0.1.3680043.8.498.47359123102728459884412887463296905395| -|blue-circle.dcm|1.2.826.0.1.3680043.8.498.13230779778012324449356534479549187420|1.2.826.0.1.3680043.8.498.77033797676425927098669402985243398207|1.2.826.0.1.3680043.8.498.13273713909719068980354078852867170114| - -> NOTE: Each of these files represent a single instance and are part of the same study. Also green-square and red-triangle are part of the same series, while blue-circle is in a separate series. - -## Prerequisites - -In order to use the DICOMWeb™ Standard APIs, you must have an instance of the Medical Imaging Server for DICOM deployed. If you have not already deployed the Medical Imaging Server, [Deploy the Medical Imaging Server to Azure](../quickstarts/deploy-via-azure.md). - -Once you have deployed an instance of the Medical Imaging Server for DICOM, retrieve the URL for your App Service: - -1. Sign into the [Azure Portal](https://portal.azure.com/). -1. Search for **App Services** and select your Medical Imaging Server for DICOM App Service. -1. Copy the **URL** of your App Service. -1. Append the version you would like to use to the end of your app service (ex. `https:///v/`) and use this as the base url for your DICOM service in all the following examples. For more information on versioning visit the [Api Versioning Documentation](../api-versioning.md). - -For this code, we'll be accessing an unsecured dev/test service. Please don't upload any private health information (PHI). - - -## Working with the Medical Imaging Server for DICOM -The DICOMweb™ standard makes heavy use of `multipart/related` HTTP requests combined with DICOM specific accept headers. Developers familiar with other REST-based APIs often find working with the DICOMweb™ standard awkward. However, once you have it up and running, it's easy to use. It just takes a little finagling to get started. - -The cURL commands each contain at least one, and sometimes two, variables that much be replaced. To simplify running the commands, do a search and replace for the following variables, replacing them with your specific values: - -* {base-url} : this is the url your created in the steps of above including the service url and version. -* {path-to-dicoms} : path to the directory which contains the red-triangle.dcm file, such as `C:/dicom-server/docs/dcms` - * Be sure to use forward slashes as separators and end the directory _without_ a trailing forward slash. - ---- -## Uploading DICOM Instances (STOW) ---- -### Store-instances-using-multipart/related - -This request intends to demonstrate how to upload DICOM files using multipart/related. - -> NOTE: The Medical Imaging Server for DICOM is more lenient than the DICOM standard. The example below, however, demonstrates a POST request that complies tightly to the standard. - -_Details:_ - -* Path: ../studies -* Method: POST -* Headers: - * `Accept: application/dicom+json` - * `Content-Type: multipart/related; type="application/dicom"` -* Body: - * `Content-Type: application/dicom` for each file uploaded, separated by a boundary value - -> Some programming languages and tools behave differently. For instance, some require you to define your own boundary. For those, you may need to use a slightly modified Content-Type header. The following have been used successfully. - > * `Content-Type: multipart/related; type="application/dicom"; boundary=ABCD1234` - > * `Content-Type: multipart/related; boundary=ABCD1234` - > * `Content-Type: multipart/related` - -`curl --location --request POST "{base-url}/studies" --header "Accept: application/dicom+json" --header "Content-Type: multipart/related; type=\"application/dicom\"" --form "file1=@{path-to-dicoms}/red-triangle.dcm;type=application/dicom" --trace-ascii "trace.txt"` - ---- - -### Store-instances-for-a-specific-study - -This request demonstrates how to upload DICOM files using multipart/related to a designated study. - -_Details:_ -* Path: ../studies/{study} -* Method: POST -* Headers: - * `Accept: application/dicom+json` - * `Content-Type: multipart/related; type="application/dicom"` -* Body: - * `Content-Type: application/dicom` for each file uploaded, separated by a boundary value - -> Some programming languages and tools behave differently. For instance, some require you to define your own boundary. For those, you may need to use a slightly modified Content-Type header. The following have been used successfully. - > * `Content-Type: multipart/related; type="application/dicom"; boundary=ABCD1234` - > * `Content-Type: multipart/related; boundary=ABCD1234` - > * `Content-Type: multipart/related` - -`curl --request POST "{base-url}/studies/1.2.826.0.1.3680043.8.498.13230779778012324449356534479549187420" --header "Accept: application/dicom+json" --header "Content-Type: multipart/related; type=\"application/dicom\"" --form "file1=@{path-to-dicoms}/blue-circle.dcm;type=application/dicom"` - ---- - -### Store-single-instance - -> NOTE: This is a non-standard API that allows the upload of a single DICOM file without the need to configure the POST for multipart/related. Although cURL handles multipart/related well, this API allows tools like Postman to upload files to the Medical Imaging Server for DICOM. - -The following is required to upload a single DICOM file. - -_Details:_ -* Path: ../studies -* Method: POST -* Headers: - * `Accept: application/dicom+json` - * `Content-Type: application/dicom` -* Body: - * Contains a single DICOM file as binary bytes. - -`curl --location --request POST "{base-url}/studies" --header "Accept: application/dicom+json" --header "Content-Type: application/dicom" --data-binary "@{path-to-dicoms}/green-square.dcm"` - ---- -## Retrieving DICOM (WADO) ---- -### Retrieve-all-instances-within-a-study - -This request retrieves all instances within a single study, and returns them as a collection of multipart/related bytes. - -_Details:_ -* Path: ../studies/{study} -* Method: GET -* Headers: - * `Accept: multipart/related; type="application/dicom"; transfer-syntax=*` - -`curl --request GET "{base-url}/studies/1.2.826.0.1.3680043.8.498.13230779778012324449356534479549187420" --header "Accept: multipart/related; type=\"application/dicom\"; transfer-syntax=*" --output "suppressWarnings.txt"` - -> This cURL command will show the downloaded bytes in the output file (suppressWarnings.txt), but these are not direct DICOM files, only a text representation of the multipart/related download. - ---- -### Retrieve-metadata-of-all-instances-in-study - -This request retrieves the metadata for all instances within a single study. - -_Details:_ -* Path: ../studies/{study}/metadata -* Method: GET -* Headers: - * `Accept: application/dicom+json` - -> This cURL command will show the downloaded bytes in the output file (suppressWarnings.txt), but these are not direct DICOM files, only a text representation of the multipart/related download. - -`curl --request GET "{base-url}/studies/1.2.826.0.1.3680043.8.498.13230779778012324449356534479549187420/metadata" --header "Accept: application/dicom+json"` - ---- -### Retrieve-all-instances-within-a-series - -This request retrieves all instances within a single series, and returns them as a collection of multipart/related bytes. - -_Details:_ -* Path: ../studies/{study}/series/{series} -* Method: GET -* Headers: - * `Accept: multipart/related; type="application/dicom"; transfer-syntax=*` - -> This cURL command will show the downloaded bytes in the output file (suppressWarnings.txt), but it is not the DICOM file, only a text representation of the multipart/related download. - -`curl --request GET "{base-url}/studies/1.2.826.0.1.3680043.8.498.13230779778012324449356534479549187420/series/1.2.826.0.1.3680043.8.498.45787841905473114233124723359129632652" --header "Accept: multipart/related; type=\"application/dicom\"; transfer-syntax=*" --output "suppressWarnings.txt"` - ---- -### Retrieve-metadata-of-all-instances-within-a-series - -This request retrieves the metadata for all instances within a single study. - -_Details:_ -* Path: ../studies/{study}/series/{series}/metadata -* Method: GET -* Headers: - * `Accept: application/dicom+json` - -`curl --request GET "{base-url}/studies/1.2.826.0.1.3680043.8.498.13230779778012324449356534479549187420/series/1.2.826.0.1.3680043.8.498.45787841905473114233124723359129632652/metadata" --header "Accept: application/dicom+json"` - ---- -### Retrieve-a-single-instance-within-a-series-of-a-study - -This request retrieves a single instance, and returns it as a DICOM formatted stream of bytes. - -_Details:_ -* Path: ../studies/{study}/series{series}/instances/{instance} -* Method: GET -* Headers: - * `Accept: application/dicom; transfer-syntax=*` - -`curl --request GET "{base-url}/studies/1.2.826.0.1.3680043.8.498.13230779778012324449356534479549187420/series/1.2.826.0.1.3680043.8.498.45787841905473114233124723359129632652/instances/1.2.826.0.1.3680043.8.498.47359123102728459884412887463296905395" --header "Accept: application/dicom; transfer-syntax=*" --output "suppressWarnings.txt"` - ---- -### Retrieve-metadata-of-a-single-instance-within-a-series-of-a-study - -This request retrieves the metadata for a single instances within a single study and series. - -_Details:_ -* Path: ../studies/{study}/series/{series}/instances/{instance}/metadata -* Method: GET -* Headers: - * `Accept: application/dicom+json` - -`curl --request GET "{base-url}/studies/1.2.826.0.1.3680043.8.498.13230779778012324449356534479549187420/series/1.2.826.0.1.3680043.8.498.45787841905473114233124723359129632652/instances/1.2.826.0.1.3680043.8.498.47359123102728459884412887463296905395/metadata" --header "Accept: application/dicom+json"` - ---- -### Retrieve-one-or-more-frames-from-a-single-instance - -This request retrieves one or more frames from a single instance, and returns them as a collection of multipart/related bytes. Multiple frames can be retrieved by passing a comma separated list of frame numbers. All DICOM instances with images have at minimum one frame, which is often just the image associated with the instance itself. - -_Details:_ -* Path: ../studies/{study}/series{series}/instances/{instance}/frames/1,2,3 -* Method: GET -* Headers: - * `Accept: multipart/related; type="application/octet-stream"; transfer-syntax=1.2.840.10008.1.2.1` (Default) or - * `Accept: multipart/related; type="application/octet-stream"; transfer-syntax=*` or - * `Accept: multipart/related; type="application/octet-stream";` - -`curl --request GET "{base-url}/studies/1.2.826.0.1.3680043.8.498.13230779778012324449356534479549187420/series/1.2.826.0.1.3680043.8.498.45787841905473114233124723359129632652/instances/1.2.826.0.1.3680043.8.498.47359123102728459884412887463296905395/frames/1" --header "Accept: multipart/related; type=\"application/octet-stream\"; transfer-syntax=1.2.840.10008.1.2.1" --output "suppressWarnings.txt"` - ---- -## Query DICOM (QIDO) - -In the following examples, we search for items using their unique identifiers. You can also search for other attributes, such as PatientName. - ---- -### Search-for-studies - -This request enables searches for one or more studies by DICOM attributes. - -> Please see the [Conformance.md](../docs/resources/conformance-statement.md) file for supported DICOM attributes. - -_Details:_ -* Path: ../studies?StudyInstanceUID={study} -* Method: GET -* Headers: - * `Accept: application/dicom+json` - -`curl --request GET "{base-url}/studies?StudyInstanceUID=1.2.826.0.1.3680043.8.498.13230779778012324449356534479549187420" --header "Accept: application/dicom+json"` - ---- -### Search-for-series - -This request enables searches for one or more series by DICOM attributes. - -> Please see the [Conformance.md](../docs/resources/conformance-statement.md) file for supported DICOM attributes. - -_Details:_ -* Path: ../series?SeriesInstanceUID={series} -* Method: GET -* Headers: - * `Accept: application/dicom+json` - -`curl --request GET "{base-url}/series?SeriesInstanceUID=1.2.826.0.1.3680043.8.498.45787841905473114233124723359129632652" --header "Accept: application/dicom+json"` - ---- -### Search-for-series-within-a-study - -This request enables searches for one or more series within a single study by DICOM attributes. - -> Please see the [Conformance.md](../docs/resources/conformance-statement.md) file for supported DICOM attributes. - -_Details:_ -* Path: ../studies/{study}/series?SeriesInstanceUID={series} -* Method: GET -* Headers: - * `Accept: application/dicom+json` - -`curl --request GET "{base-url}/studies/1.2.826.0.1.3680043.8.498.13230779778012324449356534479549187420/series?SeriesInstanceUID=1.2.826.0.1.3680043.8.498.45787841905473114233124723359129632652" --header "Accept: application/dicom+json"` - ---- -### Search-for-instances - -This request enables searches for one or more instances by DICOM attributes. - -> Please see the [Conformance.md](../docs/resources/conformance-statement.md) file for supported DICOM attributes. - -_Details:_ -* Path: ../instances?SOPInstanceUID={instance} -* Method: GET -* Headers: - * `Accept: application/dicom+json` - -`curl --request GET "{base-url}/instances?SOPInstanceUID=1.2.826.0.1.3680043.8.498.47359123102728459884412887463296905395" --header "Accept: application/dicom+json"` - ---- -### Search-for-instances-within-a-study - -This request enables searches for one or more instances within a single study by DICOM attributes. - -> Please see the [Conformance.md](../docs/resources/conformance-statement.md) file for supported DICOM attributes. - -_Details:_ -* Path: ../studies/{study}/instances?SOPInstanceUID={instance} -* Method: GET -* Headers: - * `Accept: application/dicom+json` - -`curl --request GET "{base-url}/studies/1.2.826.0.1.3680043.8.498.13230779778012324449356534479549187420/instances?SOPInstanceUID=1.2.826.0.1.3680043.8.498.47359123102728459884412887463296905395" --header "Accept: application/dicom+json"` - ---- -### Search-for-instances-within-a-study-and-series - -This request enables searches for one or more instances within a single study and single series by DICOM attributes. - -> Please see the [Conformance.md](../docs/resources/conformance-statement.md) file for supported DICOM attributes. - -_Details:_ -* Path: ../studies/{study}/series/{series}/instances?SOPInstanceUID={instance} -* Method: GET -* Headers: - * `Accept: application/dicom+json` - -`curl --request GET "{base-url}/studies/1.2.826.0.1.3680043.8.498.13230779778012324449356534479549187420/series/1.2.826.0.1.3680043.8.498.45787841905473114233124723359129632652/instances?SOPInstanceUID=1.2.826.0.1.3680043.8.498.47359123102728459884412887463296905395" --header "Accept: application/dicom+json"` - ---- -## Delete DICOM ---- -### Delete-a-specific-instance-within-a-study-and-series - -This request deletes a single instance within a single study and single series. - -> Delete is not part of the DICOM standard, but has been added for convenience. - -_Details:_ -* Path: ../studies/{study}/series/{series}/instances/{instance} -* Method: DELETE -* Headers: No special headers needed - -`curl --request DELETE "{base-url}/studies/1.2.826.0.1.3680043.8.498.13230779778012324449356534479549187420/series/1.2.826.0.1.3680043.8.498.45787841905473114233124723359129632652/instances/1.2.826.0.1.3680043.8.498.47359123102728459884412887463296905395"` - ---- -### Delete-a-specific-series-within-a-study - -This request deletes a single series (and all child instances) within a single study. - -> Delete is not part of the DICOM standard, but has been added for convenience. - -_Details:_ -* Path: ../studies/{study}/series/{series} -* Method: DELETE -* Headers: No special headers needed - -`curl --request DELETE "{base-url}/studies/1.2.826.0.1.3680043.8.498.13230779778012324449356534479549187420/series/1.2.826.0.1.3680043.8.498.45787841905473114233124723359129632652"` - ---- -### Delete-a-specific-study - -This request deletes a single study (and all child series and instances). - -> Delete is not part of the DICOM standard, but has been added for convenience. - -_Details:_ -* Path: ../studies/{study} -* Method: DELETE -* Headers: No special headers needed - -`curl --request DELETE "{base-url}/studies/1.2.826.0.1.3680043.8.498.13230779778012324449356534479549187420"` diff --git a/docs/tutorials/use-dicom-web-standard-apis-with-python.md b/docs/tutorials/use-dicom-web-standard-apis-with-python.md deleted file mode 100644 index 1c2b21a10e..0000000000 --- a/docs/tutorials/use-dicom-web-standard-apis-with-python.md +++ /dev/null @@ -1,551 +0,0 @@ -# Use DICOMweb™ Standard APIs with Python - -> This document is a Markdown export from a Jupyter Notebook found [here](../resources/use-dicom-web-standard-apis-with-python.ipynb). By opening the notebook in Jupyter, you can walk through the examples in a fully interactive experience. - -This tutorial uses Python to demonstrate working with the Medical Imaging Server for DICOM. - -For the tutorial we will use the DICOM files here: [Sample DICOM files](../dcms). The file name, studyUID, seriesUID and instanceUID of the sample DICOM files is as follows: - -| File | StudyUID | SeriesUID | InstanceUID | -| --- | --- | --- | ---| -|green-square.dcm|1.2.826.0.1.3680043.8.498.13230779778012324449356534479549187420|1.2.826.0.1.3680043.8.498.45787841905473114233124723359129632652|1.2.826.0.1.3680043.8.498.12714725698140337137334606354172323212| -|red-triangle.dcm|1.2.826.0.1.3680043.8.498.13230779778012324449356534479549187420|1.2.826.0.1.3680043.8.498.45787841905473114233124723359129632652|1.2.826.0.1.3680043.8.498.47359123102728459884412887463296905395| -|blue-circle.dcm|1.2.826.0.1.3680043.8.498.13230779778012324449356534479549187420|1.2.826.0.1.3680043.8.498.77033797676425927098669402985243398207|1.2.826.0.1.3680043.8.498.13273713909719068980354078852867170114| - -> NOTE: Each of these files represent a single instance and are part of the same study. Also green-square and red-triangle are part of the same series, while blue-circle is in a separate series. - - -## Prerequisites - -In order to use the DICOMWeb™ Standard APIs, you must have an instance of the Medical Imaging Server for DICOM deployed. If you have not already deployed the Medical Imaging Server, [Deploy the Medical Imaging Server to Azure](../quickstarts/deploy-via-azure.md). - -Once you have deployed an instance of the Medical Imaging Server for DICOM, retrieve the URL for your App Service: - -1. Sign into the [Azure Portal](https://portal.azure.com/). -1. Search for **App Services** and select your Medical Imaging Server for DICOM App Service. -1. Copy the **URL** of your App Service. - -For this code, we'll be accessing an unsecured dev/test service. Please don't upload any private health information (PHI). - - -## Working with the Medical Imaging Server for DICOM -The DICOMweb™ standard makes heavy use of `multipart/related` HTTP requests combined with DICOM specific accept headers. Developers familiar with other REST-based APIs often find working with the DICOMweb™ standard awkward. However, once you have it up and running, it's easy to use. It just takes a little finagling to get started. - -### Import the appropriate Python libraries - -First, import the necessary Python libraries. - -We've chosen to implement this example using the synchronous `requests` library. For asnychronous support, consider using `httpx` or another async library. Additionally, we're importing two supporting functions from `urllib3` to support working with `multipart/related` requests. - - -```python -import requests -import pydicom -from pathlib import Path -from urllib3.filepost import encode_multipart_formdata, choose_boundary -``` - -### Configure user-defined variables to be used throughout -Replace all variable values wrapped in { } with your own values. Additionally, validate that any constructed variables are correct. For instance, `base_url` is constructed using the default URL for Azure App Service and then appended with the version of the REST API being used. For more information on versioning visit the [Api Versioning Documentation](../api-versioning.md). If you're using a custom URL, you'll need to override that value with your own. - - -```python -dicom_server_name = "{server-name}" -path_to_dicoms_dir = "{path to the folder that includes green-square.dcm and other dcm files}" -version = "{version of rest api to use}" - -base_url = f"https://{dicom_server_name}.azurewebsites.net/v{version}" - -study_uid = "1.2.826.0.1.3680043.8.498.13230779778012324449356534479549187420"; #StudyInstanceUID for all 3 examples -series_uid = "1.2.826.0.1.3680043.8.498.45787841905473114233124723359129632652"; #SeriesInstanceUID for green-square and red-triangle -instance_uid = "1.2.826.0.1.3680043.8.498.47359123102728459884412887463296905395"; #SOPInstanceUID for red-triangle -``` - -### Create supporting methods to support `multipart\related` -The `Requests` library (and most Python libraries) do not work with `multipart\related` in a way that supports DICOMweb™. Because of this, we need to add a few methods to support working with DICOM files. - -`encode_multipart_related` takes a set of fields (in the DICOM case, these are generally Part 10 dcm files) and an optional user defined boundary. It returns both the full body, along with the content_type, which can be used - - - -```python -def encode_multipart_related(fields, boundary=None): - if boundary is None: - boundary = choose_boundary() - - body, _ = encode_multipart_formdata(fields, boundary) - content_type = str('multipart/related; boundary=%s' % boundary) - - return body, content_type -``` - -### Create a `requests` session -Create a `requests` session, called `client`, that will be used to communicate with the Medical Imaging Server for DICOM. - - -```python -client = requests.session() -``` - --------------------- -## Uploading DICOM Instances (STOW) - -The following examples highlight persisting DICOM files. - -### Store-instances-using-multipart/related - -This demonstrates how to upload a single DICOM file. This uses a bit of a Python hack to pre-load the DICOM file (as bytes) into memory. By passing an array of files to the fields parameter ofencode_multipart_related, multiple files can be uploaded in a single POST. This is sometimes used to upload a complete Series or Study. - -_Details:_ - -* Path: ../studies -* Method: POST -* Headers: - * `Accept: application/dicom+json` - * `Content-Type: multipart/related; type="application/dicom"` -* Body: - * `Content-Type: application/dicom` for each file uploaded, separated by a boundary value - -> Some programming languages and tools behave differently. For instance, some require you to define your own boundary. For those, you may need to use a slightly modified Content-Type header. The following have been used successfully. - > * `Content-Type: multipart/related; type="application/dicom"; boundary=ABCD1234` - > * `Content-Type: multipart/related; boundary=ABCD1234` - > * `Content-Type: multipart/related` - - -```python -#upload blue-circle.dcm -filepath = Path(path_to_dicoms_dir).joinpath('blue-circle.dcm') - -# Hack. Need to open up and read through file and load bytes into memory -with open(filepath,'rb') as reader: - rawfile = reader.read() -files = {'file': ('dicomfile', rawfile, 'application/dicom')} - -#encode as multipart_related -body, content_type = encode_multipart_related(fields = files) - -headers = {'Accept':'application/dicom+json', "Content-Type":content_type} - -url = f'{base_url}/studies' -response = client.post(url, body, headers=headers, verify=False) -``` - -### Store-instances-for-a-specific-study - -This demonstrates how to upload a multiple DICOM files into the specified study. This uses a bit of a Python hack to pre-load the DICOM file (as bytes) into memory. - -By passing an array of files to the fields parameter of `encode_multipart_related`, multiple files can be uploaded in a single POST. This is sometimes used to upload a complete Series or Study. - -_Details:_ -* Path: ../studies/{study} -* Method: POST -* Headers: - * `Accept: application/dicom+json` - * `Content-Type: multipart/related; type="application/dicom"` -* Body: - * `Content-Type: application/dicom` for each file uploaded, separated by a boundary value - - -```python - -filepath_red = Path(path_to_dicoms_dir).joinpath('red-triangle.dcm') -filepath_green = Path(path_to_dicoms_dir).joinpath('green-square.dcm') - -# Hack. Need to open up and read through file and load bytes into memory -with open(filepath_red,'rb') as reader: - rawfile_red = reader.read() -with open(filepath_green,'rb') as reader: - rawfile_green = reader.read() - -files = {'file_red': ('dicomfile', rawfile_red, 'application/dicom'), - 'file_green': ('dicomfile', rawfile_green, 'application/dicom')} - -#encode as multipart_related -body, content_type = encode_multipart_related(fields = files) - -headers = {'Accept':'application/dicom+json', "Content-Type":content_type} - -url = f'{base_url}/studies' -response = client.post(url, body, headers=headers, verify=False) -``` - -### Store single instance (non-standard) - -This demonstrates how to upload a single DICOM file. This non-standard API endpoint simplifies uploading a single file as binary bytes sent in the body of a request - -_Details:_ -* Path: ../studies -* Method: POST -* Headers: - * `Accept: application/dicom+json` - * `Content-Type: application/dicom` -* Body: - * Contains a single DICOM file as binary bytes. - -```python -#upload blue-circle.dcm -filepath = Path(path_to_dicoms_dir).joinpath('blue-circle.dcm') - -# Hack. Need to open up and read through file and load bytes into memory -with open(filepath,'rb') as reader: - body = reader.read() - -headers = {'Accept':'application/dicom+json', 'Content-Type':'application/dicom'} - -url = f'{base_url}/studies' -response = client.post(url, body, headers=headers, verify=False) -response # response should be a 409 Conflict if the file was already uploaded in the above request -``` - --------------------- -## Retrieve DICOM Instances (WADO) - -The following examples highlight retrieving DICOM instances. - ------- - -### Retrieve all instances within a study - -This retrieves all instances within a single study. - -_Details:_ -* Path: ../studies/{study} -* Method: GET -* Headers: - * `Accept: multipart/related; type="application/dicom"; transfer-syntax=*` - -All three of the dcm files that we uploaded previously are part of the same study so the response should return all 3 instances. Validate that the response has a status code of OK and that all three instances are returned. - -```python -url = f'{base_url}/studies/{study_uid}' -headers = {'Accept':'multipart/related; type="application/dicom"; transfer-syntax=*'} - -response = client.get(url, headers=headers) #, verify=False) -``` - -### Use the retrieved instances - -The instances are retrieved as binary bytes. You can loop through the returned items and convert the bytes into a file-like structure which can be read by `pydicom`. - - -```python -import requests_toolbelt as tb -from io import BytesIO - -mpd = tb.MultipartDecoder.from_response(response) -for part in mpd.parts: - # Note that the headers are returned as binary! - print(part.headers[b'content-type']) - - # You can convert the binary body (of each part) into a pydicom DataSet - # And get direct access to the various underlying fields - dcm = pydicom.dcmread(BytesIO(part.content)) - print(dcm.PatientName) - print(dcm.SOPInstanceUID) -``` - - -### Retrieve metadata of all instances in study - -This request retrieves the metadata for all instances within a single study. - -_Details:_ -* Path: ../studies/{study}/metadata -* Method: GET -* Headers: - * `Accept: application/dicom+json` - -All three of the dcm files that we uploaded previously are part of the same study so the response should return the metadata for all 3 instances. Validate that the response has a status code of OK and that all the metadata is returned. - -```python -url = f'{base_url}/studies/{study_uid}/metadata' -headers = {'Accept':'application/dicom+json'} - -response = client.get(url, headers=headers) #, verify=False) -``` - -### Retrieve all instances within a series - -This retrieves all instances within a single series. - -_Details:_ -* Path: ../studies/{study}/series/{series} -* Method: GET -* Headers: - * `Accept: multipart/related; type="application/dicom"; transfer-syntax=*` - -This series has 2 instances (green-square and red-triangle), so the response should return both instances. Validate that the response has a status code of OK and that both instances are returned. - -```python -url = f'{base_url}/studies/{study_uid}/series/{series_uid}' -headers = {'Accept':'multipart/related; type="application/dicom"; transfer-syntax=*'} - -response = client.get(url, headers=headers) #, verify=False) -``` - - -### Retrieve metadata of all instances in series - -This request retrieves the metadata for all instances within a single series. - -_Details:_ -* Path: ../studies/{study}/series/{series}/metadata -* Method: GET -* Headers: - * `Accept: application/dicom+json` - -This series has 2 instances (green-square and red-triangle), so the response should return metatdata for both instances. Validate that the response has a status code of OK and that both instances metadata are returned. - -```python -url = f'{base_url}/studies/{study_uid}/series/{series_uid}/metadata' -headers = {'Accept':'application/dicom+json'} - -response = client.get(url, headers=headers) #, verify=False) -``` - -### Retrieve a single instance within a series of a study - -This request retrieves a single instance. - -_Details:_ -* Path: ../studies/{study}/series{series}/instances/{instance} -* Method: GET -* Headers: - * `Accept: application/dicom; transfer-syntax=*` - -This should only return the instance red-triangle. Validate that the response has a status code of OK and that the instance is returned. - - -```python -url = f'{base_url}/studies/{study_uid}/series/{series_uid}/instances/{instance_uid}' -headers = {'Accept':'application/dicom; transfer-syntax=*'} - -response = client.get(url, headers=headers) #, verify=False) -``` - - -### Retrieve metadata of a single instance within a series of a study - -This request retrieves the metadata for a single instances within a single study and series. - -_Details:_ -* Path: ../studies/{study}/series/{series}/instances/{instance}/metadata -* Method: GET -* Headers: - * `Accept: application/dicom+json` - -This should only return the metatdata for the instance red-triangle. Validate that the response has a status code of OK and that the metadata is returned. - -```python -url = f'{base_url}/studies/{study_uid}/series/{series_uid}/instances/{instance_uid}/metadata' -headers = {'Accept':'application/dicom+json'} - -response = client.get(url, headers=headers) #, verify=False) -``` - -### Retrieve one or more frames from a single instance - -This request retrieves one or more frames from a single instance. - -_Details:_ -* Path: ../studies/{study}/series{series}/instances/{instance}/frames/1,2,3 -* Method: GET -* Headers: - * `Accept: multipart/related; type="application/octet-stream"; transfer-syntax=1.2.840.10008.1.2.1` (Default) or - * `Accept: multipart/related; type="application/octet-stream"; transfer-syntax=*` or - * `Accept: multipart/related; type="application/octet-stream";` - -This should return the only frame from the red-triangle. Validate that the response has a status code of OK and that the frame is returned. - -```python -url = f'{base_url}/studies/{study_uid}/series/{series_uid}/instances/{instance_uid}/frames/1' -headers = {'Accept':'multipart/related; type="application/octet-stream"; transfer-syntax=*'} - -response = client.get(url, headers=headers) #, verify=False) -``` - --------------------- -## Query DICOM (QIDO) - -In the following examples, we search for items using their unique identifiers. You can also search for other attributes, such as PatientName. - -> Please see the [Conformance Statement](../resources/conformance-statement.md#supported-search-parameters) file for supported DICOM attributes. - ---- -### Search for studies - -This request searches for one or more studies by DICOM attributes. - -_Details:_ -* Path: ../studies?StudyInstanceUID={study} -* Method: GET -* Headers: - * `Accept: application/dicom+json` - -Validate that response includes 1 study and that response code is OK. - - -```python -url = f'{base_url}/studies' -headers = {'Accept':'application/dicom+json'} -params = {'StudyInstanceUID':study_uid} - -response = client.get(url, headers=headers, params=params) #, verify=False) -``` - -### Search for series - -This request searches for one or more series by DICOM attributes. - -_Details:_ -* Path: ../series?SeriesInstanceUID={series} -* Method: GET -* Headers: - * `Accept: application/dicom+json` - -Validate that response includes 1 series and that response code is OK. - -```python -url = f'{base_url}/series' -headers = {'Accept':'application/dicom+json'} -params = {'SeriesInstanceUID':series_uid} - -response = client.get(url, headers=headers, params=params) #, verify=False) -``` - -### Search for series within a study - -This request searches for one or more series within a single study by DICOM attributes. - -_Details:_ -* Path: ../studies/{study}/series?SeriesInstanceUID={series} -* Method: GET -* Headers: - * `Accept: application/dicom+json` - -Validate that response includes 1 series and that response code is OK. - -```python -url = f'{base_url}/studies/{study_uid}/series' -headers = {'Accept':'application/dicom+json'} -params = {'SeriesInstanceUID':series_uid} - -response = client.get(url, headers=headers, params=params) #, verify=False) -``` - - -### Search for instances - -This request searches for one or more instances by DICOM attributes. - -_Details:_ -* Path: ../instances?SOPInstanceUID={instance} -* Method: GET -* Headers: - * `Accept: application/dicom+json` - -Validate that response includes 1 instance and that response code is OK. - -```python -url = f'{base_url}/instances' -headers = {'Accept':'application/dicom+json'} -params = {'SOPInstanceUID':instance_uid} - -response = client.get(url, headers=headers, params=params) #, verify=False) -``` - -### Search for instances within a study - -This request searches for one or more instances within a single study by DICOM attributes. - -_Details:_ -* Path: ../studies/{study}/instances?SOPInstanceUID={instance} -* Method: GET -* Headers: - * `Accept: application/dicom+json` - -Validate that response includes 1 instance and that response code is OK. - -```python -url = f'{base_url}/studies/{study_uid}/instances' -headers = {'Accept':'application/dicom+json'} -params = {'SOPInstanceUID':instance_uid} - -response = client.get(url, headers=headers, params=params) #, verify=False) -``` - -### Search for instances within a study and series - -This request searches for one or more instances within a single study and single series by DICOM attributes. - -_Details:_ -* Path: ../studies/{study}/series/{series}/instances?SOPInstanceUID={instance} -* Method: GET -* Headers: - * `Accept: application/dicom+json` - -Validate that response includes 1 instance and that response code is OK. - -```python -url = f'{base_url}/studies/{study_uid}/series/{series_uid}/instances' -headers = {'Accept':'application/dicom+json'} -params = {'SOPInstanceUID':instance_uid} - -response = client.get(url, headers=headers, params=params) #, verify=False) -``` - ------------------ -## Delete DICOM - -> NOTE: Delete is not part of the DICOM standard, but has been added for convenience. - -A 204 response code is returned when the deletion is successful. A 404 response code is returned if the item(s) have never existed or have already been deleted. - -### Delete a specific instance within a study and series - -This request deletes a single instance within a single study and single series. - -_Details:_ -* Path: ../studies/{study}/series/{series}/instances/{instance} -* Method: DELETE -* Headers: No special headers needed - -This deletes the red-triangle instance from the server. If it is successful the response status code contains no content. - -```python -#headers = {'Accept':'anything/at+all'} -url = f'{base_url}/studies/{study_uid}/series/{series_uid}/instances/{instance_uid}' -response = client.delete(url) -``` - -### Delete a specific series within a study - -This request deletes a single series (and all child instances) within a single study. - -_Details:_ -* Path: ../studies/{study}/series/{series} -* Method: DELETE -* Headers: No special headers needed - -This deletes the green-square instance (it is the only element left in the series) from the server. If it is successful the response status code contains no content. - -```python -#headers = {'Accept':'anything/at+all'} -url = f'{base_url}/studies/{study_uid}/series/{series_uid}' -response = client.delete(url) -``` - -### Delete a specific study - -This request deletes a single study (and all child series and instances). - -_Details:_ -* Path: ../studies/{study} -* Method: DELETE -* Headers: No special headers needed - -```python -#headers = {'Accept':'anything/at+all'} -url = f'{base_url}/studies/{study_uid}' -response = client.delete(url) -``` diff --git a/docs/tutorials/use-the-medical-imaging-server-apis.md b/docs/tutorials/use-the-medical-imaging-server-apis.md deleted file mode 100644 index f22cb0215d..0000000000 --- a/docs/tutorials/use-the-medical-imaging-server-apis.md +++ /dev/null @@ -1,52 +0,0 @@ -# Use DICOMweb™ Standard APIs with Medical Imaging Server for DICOM - -This tutorial gives on overview of how to use the DICOMweb™ Standard APIs with the Medical Imaging Server for DICOM. - -The Medical Imaging Server for DICOM supports a subset of the DICOMweb™ Standard. Support includes: - -- Store (STOW-RS) -- Retrieve (WADO-RS) -- Search (QIDO-RS) - -Additionally, the following non-standard API(s) are supported: - -- Delete -- Change Feed - -You can learn more about our support of the various DICOM Web Standard APIs in our [Conformance Statement](../resources/conformance-statement.md). - -## Prerequisites - -In order to use the DICOMweb™ Standard APIs, you must have an instance of the Medical Imaging Server for DICOM deployed. If you have not already deployed the Medical Imaging Server, [Deploy the Medical Imaging Server to Azure](../quickstarts/deploy-via-azure.md). - -Once deployment is complete, you can use the Azure Portal to navigate to the newly created App Service to see the details and the service url. Make sure to specify the version as part of the url when making requests. More information can be found in the [Api Versioning Documentation](../api-versioning.md) - -## Overview of various methods to use with Medical Imaging Server for DICOM - -Because the Medical Imaging Server for DICOM is exposed as a REST API, you can access it using any modern development language. For language-agnostic information on working with the service, please refer to our [Conformance Statement](../resources/conformance-statement.md). - -To see language-specific examples, please see the examples below. Alternatively, if you open the Postman Collection, you can see examples in several languages including Go, Java, Javascript, C#, PHP, C, NodeJS, Objective-C, OCaml, PowerShell, Python, Ruby, and Swift. - -### C# - -The C# examples use the library included in this repo to simplify access to the API. Refer to the [C# examples](../tutorials/use-dicom-web-standard-apis-with-c%23.md) to learn how to use C# with the Medical Imaging Server for DICOM. - -### cURL - -cURL is a common command line tool for calling web endpoints that is available for nearly any operating system. [Download cURL](https://curl.haxx.se/download.html) to get started. To use the examples, you'll need to replace the server name with your instance name, and download the [example DICOM files](../dcms) in this repo to a known location on your local file system. Refer to the [cURL examples](../tutorials/use-dicom-web-standard-apis-with-curl.md) to learn how to use cURL with the Medical Imaging Server for DICOM. - -### Postman - -Postman is an excellent tool for designing, building and testing REST APIs. [Download Postman](https://www.postman.com/downloads/) to get started. You can learn how to effectively use Postman at the [Postman learning site](https://learning.postman.com/). - -One important caveat with Postman and the DICOMweb™ Standard: Postman can only support uploading DICOM files using the single part payload defined in the DICOM standard. This is because Postman cannot support custom separators in a multipart/related POST request. For more information, please see [https://github.com/postmanlabs/postman-app-support/issues/576](https://github.com/postmanlabs/postman-app-support/issues/576) for more information on this bug. Thus all examples in the Postman collection for uploading DICOM documents using a multipart request are prefixed with [will not work - see description]. The examples for uploading using a single part request are included in the collection and are prefixed with "Store-Single-Instance". - -To use the Postman collection, you'll need to download the collection locally and import the collection through Postman, which are available here: [Postman Collection Examples](../resources/Conformance-as-Postman.postman_collection.json). In the collection when you are asked to specify the base url, it is the full url of your service appended with the version (ex. `https:///v/`) For more information on versioning visit the [Api Versioning Documentation](../api-versioning.md). - -## Summary - -This tutorial provided an overview of the APIs supported by the Medical Imaging Server for DICOM. Get started using these APIs with the following tools: - -- [Use DICOM Web Standard APIs with C#](../tutorials/use-dicom-web-standard-apis-with-c%23.md) -- [Use DICOM Web Standard APIs with cURL](../tutorials/use-dicom-web-standard-apis-with-curl.md) -- [Use DICOM Web Standard APIs with Postman Example Collection](../resources/Conformance-as-Postman.postman_collection.json) diff --git a/forks/.globalconfig b/forks/.globalconfig deleted file mode 100644 index f2b66b7548..0000000000 --- a/forks/.globalconfig +++ /dev/null @@ -1,26 +0,0 @@ -# Global AnalyzerConfig file -# For details: https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/configuration-files -# For rules: https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference -is_global = true -global_level = 2 - -# Code Quality Rules -dotnet_diagnostic.CA1012.severity = none # Abstract types should not have public constructors -dotnet_diagnostic.CA1054.severity = none # URI parameters should not be strings -dotnet_diagnostic.CA1062.severity = none # Validate arguments of public methods -dotnet_diagnostic.CA1200.severity = none # Avoid using cref tags with a prefix -dotnet_diagnostic.CA1305.severity = none # Specify IFormatProvider -dotnet_diagnostic.CA1707.severity = none # Identifiers should not contain underscores -dotnet_diagnostic.CA1802.severity = none # Use Literals Where Appropriate -dotnet_diagnostic.CA1822.severity = none # Mark members as static - -# C# Compiler Rules -dotnet_diagnostic.CS1572.severity = none # XML comment on 'construct' has a param tag for 'parameter', but there is no parameter by that name -dotnet_diagnostic.CS1573.severity = none # Parameter 'parameter' has no matching param tag in the XML comment for 'parameter' (but other parameters do) - -# Code Style Rules -dotnet_diagnostic.IDE0006.severity = none # Copyright File Header -dotnet_diagnostic.IDE0055.severity = none # Fix formatting -dotnet_diagnostic.IDE1006.severity = none # Naming rule violation: Prefix '_' is not expected -dotnet_diagnostic.IDE0161.severity = none # Convert to file-scoped namespace -dotnet_diagnostic.IDE0073.severity = none # Copyright File Header diff --git a/forks/Microsoft.Health.FellowOakDicom/Core/DicomElement.cs b/forks/Microsoft.Health.FellowOakDicom/Core/DicomElement.cs deleted file mode 100644 index 57ab0e017d..0000000000 --- a/forks/Microsoft.Health.FellowOakDicom/Core/DicomElement.cs +++ /dev/null @@ -1,286 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Globalization; -using System.Linq; -using System.Reflection; -using FellowOakDicom; -using FellowOakDicom.IO.Buffer; - -namespace Microsoft.Health.FellowOakDicom.Core; - - -/// Decimal String (DS) -public class DicomDecimalString : DicomMultiStringOrNumberElement -{ - #region FIELDS - - private static readonly Func _toDecimalValue = new Func( - x => decimal.Parse(x, NumberStyles.Any, CultureInfo.InvariantCulture)); - - #endregion - - - #region Public Constructors - - public DicomDecimalString(DicomTag tag, params decimal[] values) - : base(tag, ToDecimalString, _toDecimalValue, values) - { - } - - public DicomDecimalString(DicomTag tag, params string[] values) - : base(tag, ToDecimalString, _toDecimalValue, values) - { - } - - public DicomDecimalString(DicomTag tag, IByteBuffer data) - : base(tag, ToDecimalString, _toDecimalValue, data) - { - } - - #endregion - - #region Public Properties - - public override DicomVR ValueRepresentation => DicomVR.DS; - - #endregion - - #region Public Members - - public static string ToDecimalString(decimal value) - { - var valueString = value.ToString(CultureInfo.InvariantCulture); - if (valueString.Length > 16) - { - valueString = value.ToString("G11", CultureInfo.InvariantCulture); - } - return valueString; - } - - #endregion - -} - - -/// Integer String (IS) -public class DicomIntegerString : DicomMultiStringOrNumberElement -{ - #region FIELDS - - private static readonly Func _toIntegerString = new Func( - x => x.ToString(CultureInfo.InvariantCulture)); - - private static readonly Func _toIntegerValue = new Func( - x => int.Parse(x, NumberStyles.Integer | NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture)); - - #endregion - - #region Public Constructors - - public DicomIntegerString(DicomTag tag, params int[] values) - : base(tag, _toIntegerString, _toIntegerValue, values) - { - } - - public DicomIntegerString(DicomTag tag, params string[] values) - : base(tag, _toIntegerString, _toIntegerValue, values) - { - } - - public DicomIntegerString(DicomTag tag, IByteBuffer data) - : base(tag, _toIntegerString, _toIntegerValue, data) - { - } - - #endregion - - #region Public Properties - - public override DicomVR ValueRepresentation => DicomVR.IS; - - #endregion -} - - -/// Signed Very Long (SV) -public class DicomSignedVeryLong : DicomMultiStringOrNumberElement -{ - #region FIELDS - - private static readonly Func _toLongString = new Func(x => x.ToString()); - - private static readonly Func _toLongValue = new Func( - x => long.Parse(x, NumberStyles.Any, CultureInfo.InvariantCulture)); - - #endregion - - #region Public Constructors - - public DicomSignedVeryLong(DicomTag tag, params long[] values) - : base(tag, _toLongString, _toLongValue, values) - { - } - - public DicomSignedVeryLong(DicomTag tag, IByteBuffer data) - : base(tag, _toLongString, _toLongValue, data) - { - } - - #endregion - - #region Public Properties - - public override DicomVR ValueRepresentation => DicomVR.SV; - - #endregion -} - - -/// Unsigned Very Long (UV) -public class DicomUnsignedVeryLong : DicomMultiStringOrNumberElement -{ - #region FIELDS - - private static readonly Func _toUnsignedLongString = new Func(x => x.ToString()); - - private static readonly Func _toUnsignedLongValue = new Func( - x => ulong.Parse(x, NumberStyles.Any, CultureInfo.InvariantCulture)); - - #endregion - - #region Public Constructors - - public DicomUnsignedVeryLong(DicomTag tag, params ulong[] values) - : base(tag, _toUnsignedLongString, _toUnsignedLongValue, values) - { - } - - public DicomUnsignedVeryLong(DicomTag tag, IByteBuffer data) - : base(tag, _toUnsignedLongString, _toUnsignedLongValue, data) - { - } - - #endregion - - #region Public Properties - - public override DicomVR ValueRepresentation => DicomVR.UV; - - #endregion -} - -/// -/// Base class to handle Multi String/Number VR Types -/// e.g. DS, IS, SV, and UV -/// -public abstract class DicomMultiStringOrNumberElement : DicomMultiStringElement where TType : struct -{ - #region FIELDS - - private TType[] _values; - - private readonly Func _toNumber; - - #endregion - - #region Public Constructors - - public DicomMultiStringOrNumberElement(DicomTag tag, Func toString, Func toNumber, params TType[] values) - : base(tag, values.Select(x => toString(x)).ToArray()) - { - _toNumber = toNumber; - } - - public DicomMultiStringOrNumberElement(DicomTag tag, Func toString, Func toNumber, params string[] values) - : base(tag, values) - { - _toNumber = toNumber; - } - - public DicomMultiStringOrNumberElement(DicomTag tag, Func toString, Func toNumber, IByteBuffer data) - : base(tag, null, data) - { - _toNumber = toNumber; - } - - #endregion - - #region Public Members - - public override T Get(int item = -1) - { - // no need to parse values if returning string(s) - if (typeof(T) == typeof(string) || typeof(T) == typeof(string[])) return base.Get(item); - - if (item == -1) - { - item = 0; - } - - if (_values == null) - { - _values = base.Get().Select(x => _toNumber(x)).ToArray(); - } - - if (typeof(T).GetTypeInfo().IsArray) - { - var t = typeof(T).GetElementType(); - - if (t == typeof(T)) return (T)(object)_values; - - var tu = Nullable.GetUnderlyingType(t) ?? t; - var tmp = _values.Select(x => Convert.ChangeType(x, tu)); - - if (t == typeof(object)) return (T)(object)tmp.ToArray(); - if (t == typeof(decimal)) return (T)(object)tmp.Cast().ToArray(); - if (t == typeof(double)) return (T)(object)tmp.Cast().ToArray(); - if (t == typeof(float)) return (T)(object)tmp.Cast().ToArray(); - if (t == typeof(long)) return (T)(object)tmp.Cast().ToArray(); - if (t == typeof(int)) return (T)(object)tmp.Cast().ToArray(); - if (t == typeof(short)) return (T)(object)tmp.Cast().ToArray(); - if (t == typeof(byte)) return (T)(object)tmp.Cast().ToArray(); - if (t == typeof(ulong)) return (T)(object)tmp.Cast().ToArray(); - if (t == typeof(uint)) return (T)(object)tmp.Cast().ToArray(); - if (t == typeof(ushort)) return (T)(object)tmp.Cast().ToArray(); - if (t == typeof(decimal?)) return (T)(object)tmp.Cast().ToArray(); - if (t == typeof(double?)) return (T)(object)tmp.Cast().ToArray(); - if (t == typeof(float?)) return (T)(object)tmp.Cast().ToArray(); - if (t == typeof(long?)) return (T)(object)tmp.Cast().ToArray(); - if (t == typeof(int?)) return (T)(object)tmp.Cast().ToArray(); - if (t == typeof(short?)) return (T)(object)tmp.Cast().ToArray(); - if (t == typeof(byte?)) return (T)(object)tmp.Cast().ToArray(); - if (t == typeof(ulong?)) return (T)(object)tmp.Cast().ToArray(); - if (t == typeof(uint?)) return (T)(object)tmp.Cast().ToArray(); - if (t == typeof(ushort?)) return (T)(object)tmp.Cast().ToArray(); - } - else if (typeof(T).GetTypeInfo().IsValueType || typeof(T) == typeof(object)) - { - if (item == -1) item = 0; - if (item < 0 || item >= Count) throw new ArgumentOutOfRangeException(nameof(item), "Index is outside the range of available value items"); - - return GetValue(_values[item]); - } - - return base.Get(item); - } - - #endregion - - #region Private Members - - private T GetValue(TType val) - { - // If nullable, need to apply conversions on underlying type (#212) - var t = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T); - - if (!t.IsEnum) return (T)Convert.ChangeType(val, t); - - return (T)Enum.Parse(t, val.ToString()); - } - - #endregion -} diff --git a/forks/Microsoft.Health.FellowOakDicom/Microsoft.Health.FellowOakDicom.csproj b/forks/Microsoft.Health.FellowOakDicom/Microsoft.Health.FellowOakDicom.csproj deleted file mode 100644 index a7f8f2844a..0000000000 --- a/forks/Microsoft.Health.FellowOakDicom/Microsoft.Health.FellowOakDicom.csproj +++ /dev/null @@ -1,17 +0,0 @@ - - - - A fork of Fellow Oak's Dicom library with changes included from the next release. - $(LibraryFrameworks);netstandard2.0 - - - - - - - - - - - - diff --git a/forks/Microsoft.Health.FellowOakDicom/Properties/AssemblyInfo.cs b/forks/Microsoft.Health.FellowOakDicom/Properties/AssemblyInfo.cs deleted file mode 100644 index 2e456f0a71..0000000000 --- a/forks/Microsoft.Health.FellowOakDicom/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,10 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Resources; - -[assembly: NeutralResourcesLanguage("en-us")] -[assembly: CLSCompliant(false)] diff --git a/forks/Microsoft.Health.FellowOakDicom/Serialization/JsonDicomConverter.cs b/forks/Microsoft.Health.FellowOakDicom/Serialization/JsonDicomConverter.cs deleted file mode 100644 index 46841f3844..0000000000 --- a/forks/Microsoft.Health.FellowOakDicom/Serialization/JsonDicomConverter.cs +++ /dev/null @@ -1,1204 +0,0 @@ -// Copyright (c) 2012-2021 fo-dicom contributors. -// Licensed under the Microsoft Public License (MS-PL). - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Text.RegularExpressions; -using FellowOakDicom; -using FellowOakDicom.IO; -using FellowOakDicom.IO.Buffer; - -using DicomDecimalString = Microsoft.Health.FellowOakDicom.Core.DicomDecimalString; -using DicomIntegerString = Microsoft.Health.FellowOakDicom.Core.DicomIntegerString; -using DicomSignedVeryLong = Microsoft.Health.FellowOakDicom.Core.DicomSignedVeryLong; -using DicomUnsignedVeryLong = Microsoft.Health.FellowOakDicom.Core.DicomUnsignedVeryLong; - - -namespace Microsoft.Health.FellowOakDicom.Serialization -{ - - [Obsolete("Please use DicomJsonConverter instead.")] - public class DicomArrayJsonConverter : JsonConverter - { - private readonly bool _writeTagsAsKeywords; - - /// - /// Initialize the JsonDicomConverter. - /// - /// Whether to write the json keys as DICOM keywords instead of tags. This makes the json non-compliant to DICOM JSON. - public DicomArrayJsonConverter() - : this(false) - { - } - - /// - /// Initialize the JsonDicomConverter. - /// - /// Whether to write the json keys as DICOM keywords instead of tags. This makes the json non-compliant to DICOM JSON. - public DicomArrayJsonConverter(bool writeTagsAsKeywords) - { - _writeTagsAsKeywords = writeTagsAsKeywords; - } - - public override DicomDataset[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var datasetList = new List(); - if (reader.TokenType != JsonTokenType.StartArray) - { - return datasetList.ToArray(); - } - reader.Read(); - var conv = new DicomJsonConverter(writeTagsAsKeywords: _writeTagsAsKeywords); - while (reader.TokenType != JsonTokenType.EndArray) - { - DicomDataset ds; - switch (reader.TokenType) - { - case JsonTokenType.StartObject: - ds = conv.Read(ref reader, typeToConvert, options); - reader.AssumeAndSkip(JsonTokenType.EndObject); - break; - case JsonTokenType.Null: - ds = null; - reader.Read(); - break; - default: - throw new JsonException($"Expected either the start of an object or null but found '{reader.TokenType}'."); - } - - datasetList.Add(ds); - } - reader.Read(); - return datasetList.ToArray(); - } - - public override void Write(Utf8JsonWriter writer, DicomDataset[] value, JsonSerializerOptions options) - { - var conv = new DicomJsonConverter(writeTagsAsKeywords: _writeTagsAsKeywords); - writer.WriteStartArray(); - foreach (var ds in value) - { - conv.Write(writer, ds, options); - } - writer.WriteEndArray(); - } - - } - - /// - /// Defines the way DICOM numbers (tags with VR: IS, DS, SV and UV) should be serialized - /// - public enum NumberSerializationMode - { - /// - /// Always serialize DICOM numbers (tags with VR: IS, DS, SV and UV) as JSON numbers. - /// ⚠️ This will throw FormatException when a number can't be parsed! - /// i.e.: "00081160":{"vr":"IS","Value":[76]} - /// - AsNumber, - - /// - /// Always serialize DICOM numbers (tags with VR: IS, DS, SV and UV) as JSON strings. - /// i.e.: "00081160":{"vr":"IS","Value":["76"]} - /// - AsString, - - /// - /// Try to serialize DICOM numbers (tags with VR: IS, DS, SV and UV) as JSON numbers. If not parsable as a number, defaults back to a JSON string. - /// This won't throw an error in case a number can't be parsed. It just returns the value as a JSON string. - /// i.e.: "00081160":{"vr":"IS","Value":[76]} - /// or "00081160":{"vr":"IS","Value":["A non parsable value"]} - /// - PreferablyAsNumber - } - - /// - /// Converts a DicomDataset object to and from JSON using the NewtonSoft Json.NET library - /// - public class DicomJsonConverter : JsonConverter - { - - private readonly bool _writeTagsAsKeywords; - private readonly bool _autoValidate; - private readonly NumberSerializationMode _numberSerializationMode; - private readonly static Encoding[] _jsonTextEncodings = { Encoding.UTF8 }; - private readonly static char _personNameComponentGroupDelimiter = '='; - private readonly static string[] _personNameComponentGroupNames = { "Alphabetic", "Ideographic", "Phonetic" }; - - private delegate T GetValue(Utf8JsonReader reader); - private delegate bool TryParse(string value, out T parsed); - private delegate void WriteValue(Utf8JsonWriter writer, T value); - - - /// - /// Initialize the JsonDicomConverter. - /// - /// Whether to write the json keys as DICOM keywords instead of tags. This makes the json non-compliant to DICOM JSON. - /// Whether the content of DicomItems shall be validated when deserializing. - /// Defines how numbers should be serialized. Default 'AsNumber', will throw errors when a number is not parsable. - public DicomJsonConverter(bool writeTagsAsKeywords = false, bool autoValidate = true, NumberSerializationMode numberSerializationMode = NumberSerializationMode.AsNumber) - { - _writeTagsAsKeywords = writeTagsAsKeywords; - _autoValidate = autoValidate; - _numberSerializationMode = numberSerializationMode; - } - - #region JsonConverter overrides - - - /// - /// Writes the JSON representation of the object. - /// - /// The to write to. - /// The value. - public override void Write(Utf8JsonWriter writer, DicomDataset value, JsonSerializerOptions options) - { - if (value == null) - { - writer.WriteNullValue(); - return; - } - - writer.WriteStartObject(); - foreach (var item in value) - { - if (((uint)item.Tag & 0xffff) == 0) - { - // Group length (gggg,0000) attributes shall not be included in a DICOM JSON Model object. - continue; - } - - // Unknown or masked tags cannot be written as keywords - var unknown = item.Tag.DictionaryEntry == null - || string.IsNullOrWhiteSpace(item.Tag.DictionaryEntry.Keyword) - || - (item.Tag.DictionaryEntry.MaskTag != null && - item.Tag.DictionaryEntry.MaskTag.Mask != 0xffffffff); - if (_writeTagsAsKeywords && !unknown) - { - writer.WritePropertyName(item.Tag.DictionaryEntry.Keyword); - } - else - { - writer.WritePropertyName(item.Tag.Group.ToString("X4") + item.Tag.Element.ToString("X4")); - } - - WriteJsonDicomItem(writer, item, options); - } - writer.WriteEndObject(); - } - - - /// - /// Reads the JSON representation of the object. - /// - /// The to read from. - /// Type of the object. - /// Options to apply while reading. - /// - /// The object value. - /// - public override DicomDataset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var dataset = ReadJsonDataset(ref reader); - return dataset; - } - - - private DicomDataset ReadJsonDataset(ref Utf8JsonReader reader) - { - var dataset = _autoValidate - ? new DicomDataset() - : new DicomDataset().NotValidated(); - if (reader.TokenType != JsonTokenType.StartObject) - { - throw new JsonException($"Expected the start of an object but found '{reader.TokenType}'."); - } - reader.Read(); - - while (reader.TokenType != JsonTokenType.EndObject) - { - reader.Assume(JsonTokenType.PropertyName); - var tagstr = reader.GetString(); - DicomTag tag = ParseTag(tagstr); - reader.Read(); // move to value - var item = ReadJsonDicomItem(tag, ref reader); - dataset.Add(item); - } - - foreach (var item in dataset) - { - if (item.Tag.IsPrivate && ((item.Tag.Element & 0xff00) != 0)) - { - var privateCreatorTag = new DicomTag(item.Tag.Group, (ushort)(item.Tag.Element >> 8)); - - if (dataset.Contains(privateCreatorTag)) - { - var privateCreatorItem = dataset.GetDicomItem(privateCreatorTag); - // Based on standard https://dicom.nema.org/dicom/2013/output/chtml/part05/sect_7.8.html private creator data item should be VM = 1, but there are buggy dcms - bool isValidPrivateCreatorItem = privateCreatorItem is DicomLongString element && element.Count == 1; - // Allow de-serialization to continue if autoValidate was set to false - if (isValidPrivateCreatorItem || _autoValidate) - { - item.Tag.PrivateCreator = new DicomPrivateCreator(dataset.GetSingleValue(privateCreatorTag)); - } - } - } - } - - return dataset; - } - - /// - /// Determines whether this instance can convert the specified object type. - /// - /// Type of the object. - /// - /// true if this instance can convert the specified object type; otherwise, false. - /// - public override bool CanConvert(Type typeToConvert) - { - return typeof(DicomDataset).GetTypeInfo().IsAssignableFrom(typeToConvert.GetTypeInfo()); - } - - #endregion - - /// - /// Create an instance of a IBulkDataUriByteBuffer. Override this method to use a different IBulkDataUriByteBuffer implementation in applications. - /// - /// The URI of a bulk data element as defined in Table A.1.5-2 in PS3.19. - /// An instance of a Bulk URI Byte buffer. - protected virtual IBulkDataUriByteBuffer CreateBulkDataUriByteBuffer(string bulkDataUri) => new BulkDataUriByteBuffer(bulkDataUri); - - #region Utilities - - internal static DicomTag ParseTag(string tagstr) - { - if (Regex.IsMatch(tagstr, @"\A\b[0-9a-fA-F]+\b\Z")) - { - var group = Convert.ToUInt16(tagstr.Substring(0, 4), 16); - var element = Convert.ToUInt16(tagstr.Substring(4), 16); - var tag = new DicomTag(group, element); - return tag; - } - - return DicomDictionary.Default[tagstr]; - } - - private static DicomItem CreateDicomItem(DicomTag tag, string vr, object data) - { - DicomItem item = vr switch - { - "AE" => new DicomApplicationEntity(tag, (string[])data), - "AS" => new DicomAgeString(tag, (string[])data), - "AT" => new DicomAttributeTag(tag, ((string[])data).Select(ParseTag).ToArray()), - "CS" => new DicomCodeString(tag, (string[])data), - "DA" => new DicomDate(tag, (string[])data), - "DS" => data is IByteBuffer dataBufferDS - ? new DicomDecimalString(tag, dataBufferDS) - : new DicomDecimalString(tag, (decimal[])data), - "DT" => new DicomDateTime(tag, (string[])data), - "FD" => data is IByteBuffer dataBufferFD - ? new DicomFloatingPointDouble(tag, dataBufferFD) - : new DicomFloatingPointDouble(tag, (double[])data), - "FL" => data is IByteBuffer dataBufferFL - ? new DicomFloatingPointSingle(tag, dataBufferFL) - : new DicomFloatingPointSingle(tag, (float[])data), - "IS" => data is IByteBuffer dataBufferIS - ? new DicomIntegerString(tag, dataBufferIS) - : new DicomIntegerString(tag, (int[])data), - "LO" => new DicomLongString(tag, (string[])data), - "LT" => data is IByteBuffer dataBufferLT - ? new DicomLongText(tag, _jsonTextEncodings, dataBufferLT) - : new DicomLongText(tag, data.GetAsStringArray().GetSingleOrEmpty()), - "OB" => new DicomOtherByte(tag, (IByteBuffer)data), - "OD" => new DicomOtherDouble(tag, (IByteBuffer)data), - "OF" => new DicomOtherFloat(tag, (IByteBuffer)data), - "OL" => new DicomOtherLong(tag, (IByteBuffer)data), - "OW" => new DicomOtherWord(tag, (IByteBuffer)data), - "OV" => new DicomOtherVeryLong(tag, (IByteBuffer)data), - "PN" => new DicomPersonName(tag, (string[])data), - "SH" => new DicomShortString(tag, (string[])data), - "SL" => data is IByteBuffer dataBufferSL - ? new DicomSignedLong(tag, dataBufferSL) - : new DicomSignedLong(tag, (int[])data), - "SQ" => new DicomSequence(tag, ((DicomDataset[])data)), - "SS" => data is IByteBuffer dataBufferSS - ? new DicomSignedShort(tag, dataBufferSS) - : new DicomSignedShort(tag, (short[])data), - "ST" => data is IByteBuffer dataBufferST - ? new DicomShortText(tag, _jsonTextEncodings, dataBufferST) - : new DicomShortText(tag, data.GetAsStringArray().GetFirstOrEmpty()), - "SV" => data is IByteBuffer dataBufferSV - ? new DicomSignedVeryLong(tag, dataBufferSV) - : new DicomSignedVeryLong(tag, (long[])data), - "TM" => new DicomTime(tag, (string[])data), - "UC" => data is IByteBuffer dataBufferUC - ? new DicomUnlimitedCharacters(tag, _jsonTextEncodings, dataBufferUC) - : new DicomUnlimitedCharacters(tag, data.GetAsStringArray().SingleOrDefault()), - "UI" => new DicomUniqueIdentifier(tag, (string[])data), - "UL" => data is IByteBuffer dataBufferUL - ? new DicomUnsignedLong(tag, dataBufferUL) - : new DicomUnsignedLong(tag, (uint[])data), - "UN" => new DicomUnknown(tag, (IByteBuffer)data), - "UR" => new DicomUniversalResource(tag, data.GetAsStringArray().GetSingleOrEmpty()), - "US" => data is IByteBuffer dataBufferUS - ? new DicomUnsignedShort(tag, dataBufferUS) - : new DicomUnsignedShort(tag, (ushort[])data), - "UT" => data is IByteBuffer dataBufferUT - ? new DicomUnlimitedText(tag, _jsonTextEncodings, dataBufferUT) - : new DicomUnlimitedText(tag, data.GetAsStringArray().GetSingleOrEmpty()), - "UV" => data is IByteBuffer dataBufferUV - ? new DicomUnsignedVeryLong(tag, dataBufferUV) - : new DicomUnsignedVeryLong(tag, (ulong[])data), - _ => throw new NotSupportedException("Unsupported value representation"), - }; - return item; - } - - #endregion - - #region WriteJson helpers - - private void WriteJsonDicomItem(Utf8JsonWriter writer, DicomItem item, JsonSerializerOptions options) - { - writer.WriteStartObject(); - writer.WriteString("vr", item.ValueRepresentation.Code); - - switch (item.ValueRepresentation.Code) - { - case "PN": - WriteJsonPersonName(writer, (DicomPersonName)item); - break; - case "SQ": - WriteJsonSequence(writer, (DicomSequence)item, options); - break; - case "OB": - case "OD": - case "OF": - case "OL": - case "OV": - case "OW": - case "UN": - WriteJsonOther(writer, (DicomElement)item); - break; - case "FL": - WriteJsonElement(writer, (DicomElement)item, (w, v) => writer.WriteNumberValue(v)); - break; - case "FD": - WriteJsonElement(writer, (DicomElement)item, (w, v) => writer.WriteNumberValue(v)); - break; - case "IS": - WriteJsonAsNumberOrString(writer, (DicomElement)item, - (w, v) => writer.WriteNumberValue(v)); - break; - case "SL": - WriteJsonElement(writer, (DicomElement)item, (w, v) => writer.WriteNumberValue(v)); - break; - case "SS": - WriteJsonElement(writer, (DicomElement)item, (w, v) => writer.WriteNumberValue(v)); - break; - case "SV": - WriteJsonAsNumberOrString(writer, (DicomElement)item, - (w, v) => writer.WriteNumberValue(v)); - break; - case "UL": - WriteJsonElement(writer, (DicomElement)item, (w, v) => writer.WriteNumberValue(v)); - break; - case "US": - WriteJsonElement(writer, (DicomElement)item, (w, v) => writer.WriteNumberValue(v)); - break; - case "UV": - WriteJsonAsNumberOrString(writer, (DicomElement)item, - (w, v) => writer.WriteNumberValue(v)); - break; - case "DS": - WriteJsonAsNumberOrString(writer, (DicomElement)item, - () => WriteJsonDecimalString(writer, (DicomElement)item)); - break; - case "AT": - WriteJsonAttributeTag(writer, (DicomElement)item); - break; - default: - WriteJsonElement(writer, (DicomElement)item, (w, v) => writer.WriteStringValue(v)); - break; - } - - writer.WriteEndObject(); - } - - private void WriteJsonAsNumberOrString(Utf8JsonWriter writer, DicomElement elem, WriteValue numberValueWriter) - => WriteJsonAsNumberOrString(writer, elem, () => WriteJsonElement(writer, elem, numberValueWriter)); - - private void WriteJsonAsNumberOrString(Utf8JsonWriter writer, DicomElement elem, Action numberWriterAction) - { - if (_numberSerializationMode == NumberSerializationMode.AsString) - { - WriteJsonElement(writer, elem, (w, v) => writer.WriteStringValue(v)); - } - else - { - try - { - numberWriterAction(); - } - catch (Exception ex) when (ex is FormatException || ex is OverflowException) - { - if (_numberSerializationMode == NumberSerializationMode.PreferablyAsNumber) - { - WriteJsonElement(writer, elem, (w, v) => writer.WriteStringValue(v)); - } - else - { - throw; - } - } - } - } - - - private static void WriteJsonDecimalString(Utf8JsonWriter writer, DicomElement elem) - { - if (elem.Count == 0) return; - - var writerActions = new List - { - () => writer.WritePropertyName("Value"), - writer.WriteStartArray - }; - - foreach (var val in elem.Get()) - { - if (string.IsNullOrEmpty(val)) - { - writerActions.Add(writer.WriteNullValue); - } - else - { - var fix = FixDecimalString(val); - if (TryParseULong(fix, out ulong xulong)) - { - writerActions.Add(() => writer.WriteNumberValue(xulong)); - } - else if (TryParseLong(fix, out long xlong)) - { - writerActions.Add(() => writer.WriteNumberValue(xlong)); - } - else if (TryParseDecimal(fix, out decimal xdecimal)) - { - writerActions.Add(() => writer.WriteNumberValue(xdecimal)); - } - else if (TryParseDouble(fix, out double xdouble)) - { - writerActions.Add(() => writer.WriteNumberValue(xdouble)); - } - else - { - throw new FormatException($"Cannot write dicom number {val} to json"); - } - } - } - writerActions.Add(writer.WriteEndArray); - - foreach (var action in writerActions) - { - action(); - } - } - - private static bool IsValidJsonNumber(string val) - { - try - { - DicomValidation.ValidateDS(val); - return true; - } - catch (DicomValidationException) - { - return false; - } - } - - /// - /// Fix-up a Dicom DS number for use with json. - /// Rationale: There is a requirement that DS numbers shall be written as json numbers in part 18.F json, but the - /// requirements on DS allows values that are not json numbers. This method "fixes" them to conform to json numbers. - /// - /// A valid DS value - /// A json number equivalent to the supplied DS value - private static string FixDecimalString(string val) - { - // trim invalid padded character - val = val.Trim().TrimEnd('\0'); - - if (IsValidJsonNumber(val)) - { - return val; - } - - if (string.IsNullOrWhiteSpace(val)) { return null; } - - val = val.Trim(); - - var negative = false; - // Strip leading superfluous plus signs - if (val[0] == '+') - { - val = val.Substring(1); - } - else if (val[0] == '-') - { - // Temporarily remove negation sign for zero-stripping later - negative = true; - val = val.Substring(1); - } - - // Strip leading superfluous zeros - if (val.Length > 1 && val[0] == '0' && val[1] != '.') - { - int i = 0; - while (i < val.Length - 1 && val[i] == '0' && val[i + 1] != '.') - { - i++; - } - - val = val.Substring(i); - } - - // Re-add negation sign - if (negative) { val = "-" + val; } - - if (IsValidJsonNumber(val)) - { - return val; - } - - throw new FormatException("Failed converting DS value to json"); - } - - private static void WriteJsonElement(Utf8JsonWriter writer, DicomElement elem, WriteValue writeValue) - { - if (elem.Count != 0) - { - T[] values = elem.Get(); - writer.WritePropertyName("Value"); - writer.WriteStartArray(); - foreach (var val in values) - { - if (val == null || (typeof(T) == typeof(string) && val.Equals(""))) - { - writer.WriteNullValue(); - } - else if (val is float f && float.IsNaN(f)) - { - writer.WriteStringValue("NaN"); - } - else - { - writeValue(writer, val); - } - } - writer.WriteEndArray(); - } - } - - private static void WriteJsonAttributeTag(Utf8JsonWriter writer, DicomElement elem) - { - if (elem.Count != 0) - { - writer.WritePropertyName("Value"); - writer.WriteStartArray(); - foreach (var val in elem.Get()) - { - if (val == null) { writer.WriteNullValue(); } - else { writer.WriteStringValue(((uint)val).ToString("X8")); } - } - writer.WriteEndArray(); - } - } - - private static void WriteJsonOther(Utf8JsonWriter writer, DicomElement elem) - { - if (elem.Buffer is IBulkDataUriByteBuffer buffer) - { - writer.WritePropertyName("BulkDataURI"); - writer.WriteStringValue(buffer.BulkDataUri); - } - else if (elem.Count != 0) - { - writer.WritePropertyName("InlineBinary"); - writer.WriteStartArray(); - writer.WriteBase64StringValue(elem.Buffer.Data); - writer.WriteEndArray(); - } - } - - private void WriteJsonSequence(Utf8JsonWriter writer, DicomSequence seq, JsonSerializerOptions options) - { - if (seq.Items.Count != 0) - { - writer.WritePropertyName("Value"); - writer.WriteStartArray(); - - foreach (var child in seq.Items) - { - Write(writer, child, options); - } - - writer.WriteEndArray(); - } - } - - private static void WriteJsonPersonName(Utf8JsonWriter writer, DicomPersonName pn) - { - if (pn.Count != 0) - { - writer.WritePropertyName("Value"); - writer.WriteStartArray(); - - foreach (var val in pn.Get()) - { - if (string.IsNullOrEmpty(val)) - { - writer.WriteNullValue(); - } - else - { - var componentGroupValues = val.Split(_personNameComponentGroupDelimiter); - int i = 0; - - writer.WriteStartObject(); - foreach (var componentGroupValue in componentGroupValues) - { - // Based on standard http://dicom.nema.org/dicom/2013/output/chtml/part18/sect_F.2.html - // 1. Empty values are skipped - // 2. Leading componentGroups even if null need to have delimiters. Trailing componentGroup delimiter can be omitted - if (!string.IsNullOrWhiteSpace(componentGroupValue)) - { - writer.WritePropertyName(_personNameComponentGroupNames[i]); - writer.WriteStringValue(componentGroupValue); - } - i++; - } - writer.WriteEndObject(); - } - } - - writer.WriteEndArray(); - } - } - - - #endregion - - - #region ReadJson helpers - - - private DicomItem ReadJsonDicomItem(DicomTag tag, ref Utf8JsonReader reader) - { - reader.AssumeAndSkip(JsonTokenType.StartObject); - var currentDepth = reader.CurrentDepth; - - reader.Assume(JsonTokenType.PropertyName); - - string vr; - var property = reader.GetString(); - if (property == "vr") - { - reader.Read(); - vr = reader.GetString(); - reader.Read(); - } - else - { - vr = FindValue(reader, "vr", "none"); - } - - if (vr == "none") { throw new JsonException("Malformed DICOM json. vr value missing"); } - - object data; - - switch (vr) - { - case "OB": - case "OD": - case "OF": - case "OL": - case "OW": - case "OV": - case "UN": - data = ReadJsonOX(ref reader); - break; - case "SQ": - data = ReadJsonSequence(ref reader); - break; - case "PN": - data = ReadJsonPersonName(ref reader); - break; - case "FL": - data = ReadJsonMultiNumber(ref reader, r => r.GetSingle()); - break; - case "FD": - data = ReadJsonMultiNumber(ref reader, r => r.GetDouble()); - break; - case "IS": - data = ReadJsonMultiNumberOrString(ref reader, r => r.GetInt32(), TryParseInt); - break; - case "SL": - data = ReadJsonMultiNumber(ref reader, r => r.GetInt32()); - break; - case "SS": - data = ReadJsonMultiNumber(ref reader, r => r.GetInt16()); - break; - case "SV": - data = ReadJsonMultiNumberOrString(ref reader, r => r.GetInt64(), TryParseLong); - break; - case "UL": - data = ReadJsonMultiNumber(ref reader, r => r.GetUInt32()); - break; - case "US": - data = ReadJsonMultiNumber(ref reader, r => r.GetUInt16()); - break; - case "UV": - data = ReadJsonMultiNumberOrString(ref reader, r => r.GetUInt64(), TryParseULong); - break; - case "DS": - data = ReadJsonMultiNumberOrString(ref reader, r => r.GetDecimal(), TryParseDecimal); - break; - default: - data = ReadJsonMultiString(ref reader); - break; - } - - // move to the end of the object - while (reader.CurrentDepth >= currentDepth && reader.Read()) - { - // skip this data - } - reader.AssumeAndSkip(JsonTokenType.EndObject); - - DicomItem item = CreateDicomItem(tag, vr, data); - return item; - } - - - private object ReadJsonMultiString(ref Utf8JsonReader reader) - { - if (reader.TokenType == JsonTokenType.EndObject) - { - return Array.Empty(); - } - string propertyname = ReadPropertyName(ref reader); - - if (propertyname == "Value") - { - return ReadJsonMultiStringValue(ref reader); - } - else if (propertyname == "BulkDataURI") - { - // JToken bulk - return ReadJsonBulkDataUri(ref reader); - } - else - { - return Array.Empty(); - } - } - - - private static string ReadPropertyName(ref Utf8JsonReader reader) - { - reader.Assume(JsonTokenType.PropertyName); - var propertyname = reader.GetString(); - reader.Read(); - return propertyname; - } - - - private static string[] ReadJsonMultiStringValue(ref Utf8JsonReader reader) - { - if (reader.TokenType == JsonTokenType.Null) - { - reader.Read(); - return Array.Empty(); - } - reader.AssumeAndSkip(JsonTokenType.StartArray); - var childStrings = new List(); - - while (reader.TokenType != JsonTokenType.EndArray) - { - if (reader.TokenType == JsonTokenType.Null) - { - childStrings.Add(null); - } - else if (reader.TokenType == JsonTokenType.String) - { - childStrings.Add(reader.GetString()); - } - else - { - // TODO: invalid. handle this? - } - reader.Read(); - } - reader.AssumeAndSkip(JsonTokenType.EndArray); - var data = childStrings.ToArray(); - return data; - } - - - private object ReadJsonMultiNumberOrString(ref Utf8JsonReader reader, GetValue getValue, TryParse tryParse) - { - if (reader.TokenType == JsonTokenType.EndObject) - { - return Array.Empty(); - } - string propertyname = ReadPropertyName(ref reader); - - if (propertyname == "Value") - { - return ReadJsonMultiNumberOrStringValue(ref reader, getValue, tryParse); - } - else if (propertyname == "BulkDataURI") - { - return ReadJsonBulkDataUri(ref reader); - } - else - { - return Array.Empty(); - } - } - - private object ReadJsonMultiNumberOrStringValue(ref Utf8JsonReader reader, GetValue getValue, TryParse tryParse) - { - if (reader.TokenType == JsonTokenType.Null) - { - reader.Read(); - return Array.Empty(); - } - reader.AssumeAndSkip(JsonTokenType.StartArray); - - var hasNonNumericString = false; - var childValues = new List(); - while (reader.TokenType != JsonTokenType.EndArray) - { - if (reader.TokenType == JsonTokenType.Number) - { - childValues.Add(getValue(reader)); - } - else if (reader.TokenType == JsonTokenType.String && reader.GetString() == "NaN") - { - childValues.Add((T)(float.NaN as object)); - } - else if (reader.TokenType == JsonTokenType.String && tryParse(reader.GetString(), out T parsed)) - { - childValues.Add(parsed); - } - else if (reader.TokenType == JsonTokenType.String && !_autoValidate) - { - hasNonNumericString = true; - childValues.Add(reader.GetString()); - } - else - { - throw new JsonException("Malformed DICOM json, number expected"); - } - reader.Read(); - } - reader.AssumeAndSkip(JsonTokenType.EndArray); - - if (hasNonNumericString) - { - var valArray = childValues.Select(x => x.ToString()).ToArray(); - return ByteConverter.ToByteBuffer(string.Join("\\", valArray)); - } - - return childValues.Cast().ToArray(); - } - - private object ReadJsonMultiNumber(ref Utf8JsonReader reader, GetValue getValue) - { - if (reader.TokenType == JsonTokenType.EndObject) - { - return Array.Empty(); - } - string propertyname = ReadPropertyName(ref reader); - - if (propertyname == "Value") - { - return ReadJsonMultiNumberValue(ref reader, getValue); - } - else if (propertyname == "BulkDataURI") - { - return ReadJsonBulkDataUri(ref reader); - } - else - { - return Array.Empty(); - } - } - - - private static T[] ReadJsonMultiNumberValue(ref Utf8JsonReader reader, GetValue getValue) - { - if (reader.TokenType == JsonTokenType.Null) - { - reader.Read(); - return Array.Empty(); - } - reader.AssumeAndSkip(JsonTokenType.StartArray); - - var childValues = new List(); - while (reader.TokenType != JsonTokenType.EndArray) - { - if (reader.TokenType == JsonTokenType.Number) - { - childValues.Add(getValue(reader)); - } - else if (reader.TokenType == JsonTokenType.String && reader.GetString() == "NaN") - { - childValues.Add((T)(float.NaN as object)); - } - else - { - throw new JsonException("Malformed DICOM json, number expected"); - } - reader.Read(); - } - reader.AssumeAndSkip(JsonTokenType.EndArray); - - var data = childValues.ToArray(); - return data; - } - - - private string[] ReadJsonPersonName(ref Utf8JsonReader reader) - { - if (reader.TokenType == JsonTokenType.EndObject) - { - return Array.Empty(); - } - var propertyName = ReadPropertyName(ref reader); - - if (propertyName == "Value") - { - if (reader.TokenType == JsonTokenType.Null) - { - reader.Read(); - return Array.Empty(); - } - else - { - reader.AssumeAndSkip(JsonTokenType.StartArray); - - var childStrings = new List(); - while (reader.TokenType != JsonTokenType.EndArray) - { - if (reader.TokenType == JsonTokenType.Null) - { - reader.Read(); - childStrings.Add(null); - } - else if (reader.TokenType == JsonTokenType.StartObject) - { - // parse - reader.Read(); // read into object - var componentGroupCount = 3; - var componentGroupValues = new string[componentGroupCount]; - while (reader.TokenType != JsonTokenType.EndObject) - { - if (reader.TokenType == JsonTokenType.PropertyName - && reader.GetString() == "Alphabetic") - { - reader.Read(); // skip propertyname - componentGroupValues[0] = reader.GetString(); // read value - } - else if (reader.TokenType == JsonTokenType.PropertyName - && reader.GetString() == "Ideographic") - { - reader.Read(); // skip propertyname - componentGroupValues[1] = reader.GetString(); // read value - } - else if (reader.TokenType == JsonTokenType.PropertyName - && reader.GetString() == "Phonetic") - { - reader.Read(); // skip propertyname - componentGroupValues[2] = reader.GetString(); // read value - } - reader.Read(); - } - - //build - StringBuilder stringBuilder = new StringBuilder(); - for (int i = 0; i < componentGroupCount; i++) - { - var val = componentGroupValues[i]; - - if (!string.IsNullOrWhiteSpace(val)) - { - stringBuilder.Append(val); - - } - stringBuilder.Append(_personNameComponentGroupDelimiter); - } - - //remove optional trailing delimiters - string pnVal = stringBuilder.ToString().TrimEnd(_personNameComponentGroupDelimiter); - - childStrings.Add(pnVal); // add value - reader.AssumeAndSkip(JsonTokenType.EndObject); - } - else - { - // TODO: invalid. handle this? - } - } - reader.AssumeAndSkip(JsonTokenType.EndArray); - var data = childStrings.ToArray(); - return data; - } - } - else - { - throw new JsonException("Malformed DICOM json, property 'Value' expected"); - } - } - - - private DicomDataset[] ReadJsonSequence(ref Utf8JsonReader reader) - { - if (reader.TokenType == JsonTokenType.EndObject) - { - return Array.Empty(); - } - var propertyName = ReadPropertyName(ref reader); - - if (propertyName == "Value") - { - reader.AssumeAndSkip(JsonTokenType.StartArray); - var childItems = new List(); - while (reader.TokenType != JsonTokenType.EndArray) - { - if (reader.TokenType == JsonTokenType.Null) - { - reader.Read(); - childItems.Add(null); - } - else if (reader.TokenType == JsonTokenType.StartObject) - { - childItems.Add(ReadJsonDataset(ref reader)); - reader.AssumeAndSkip(JsonTokenType.EndObject); - } - else - { - throw new JsonException("Malformed DICOM json, object expected"); - } - } - reader.AssumeAndSkip(JsonTokenType.EndArray); - var data = childItems.ToArray(); - return data; - } - else - { - return Array.Empty(); - } - } - - - private IByteBuffer ReadJsonOX(ref Utf8JsonReader reader) - { - if (reader.TokenType == JsonTokenType.EndObject) - { - return EmptyBuffer.Value; - } - var propertyName = ReadPropertyName(ref reader); - - if (propertyName == "InlineBinary") - { - return ReadJsonInlineBinary(ref reader); - } - else if (propertyName == "BulkDataURI") - { - return ReadJsonBulkDataUri(ref reader); - } - return EmptyBuffer.Value; - } - - - private static MemoryByteBuffer ReadJsonInlineBinary(ref Utf8JsonReader reader) - { - reader.AssumeAndSkip(JsonTokenType.StartArray); - if (reader.TokenType != JsonTokenType.String) { throw new JsonException("Malformed DICOM json. string expected"); } - var data = new MemoryByteBuffer(reader.GetBytesFromBase64()); - reader.Read(); - reader.AssumeAndSkip(JsonTokenType.EndArray); - return data; - } - - - private IBulkDataUriByteBuffer ReadJsonBulkDataUri(ref Utf8JsonReader reader) - { - if (reader.TokenType != JsonTokenType.String) { throw new JsonException("Malformed DICOM json. string expected"); } - var data = CreateBulkDataUriByteBuffer(reader.GetString()); - reader.Read(); - return data; - } - - - #endregion - - - private string FindValue(Utf8JsonReader reader, string property, string defaultValue) - { - var currentDepth = reader.CurrentDepth; - while (reader.CurrentDepth >= currentDepth) - { - if (reader.CurrentDepth == currentDepth - && reader.TokenType == JsonTokenType.PropertyName - && reader.GetString() == property) - { - reader.Read(); // move to value - return reader.GetString(); - } - reader.Read(); - } - return defaultValue; - } - - private static bool TryParseInt(string value, out int parsed) - => int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out parsed); - - private static bool TryParseDecimal(string value, out decimal parsed) - => decimal.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out parsed); - - private static bool TryParseDouble(string value, out double parsed) - => double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out parsed); - - private static bool TryParseLong(string value, out long parsed) - => long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out parsed); - - private static bool TryParseULong(string value, out ulong parsed) - => ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out parsed); - - } - - - internal static class JsonDicomConverterExtensions - { - - public static string[] GetAsStringArray(this object data) => (string[])data; - - public static string GetFirstOrEmpty(this string[] array) => array.Length > 0 ? array[0] : string.Empty; - - public static string GetSingleOrEmpty(this string[] array) => array.Length > 0 ? array.Single() : string.Empty; - - } - -} diff --git a/forks/Microsoft.Health.FellowOakDicom/Serialization/Utf8JsonReaderExtensions.cs b/forks/Microsoft.Health.FellowOakDicom/Serialization/Utf8JsonReaderExtensions.cs deleted file mode 100644 index 909c6e60a8..0000000000 --- a/forks/Microsoft.Health.FellowOakDicom/Serialization/Utf8JsonReaderExtensions.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) 2012-2021 fo-dicom contributors. -// Licensed under the Microsoft Public License (MS-PL). - -using System.Text.Json; - -namespace Microsoft.Health.FellowOakDicom.Serialization -{ - internal static class Utf8JsonReaderExtensions - { - public static void Assume(this ref Utf8JsonReader reader, JsonTokenType tokenType) - { - if (reader.TokenType != tokenType) - { - throw new JsonException($"invalid: {tokenType} expected at position {reader.TokenStartIndex}, instead found {reader.TokenType}"); - } - } - - public static void AssumeAndSkip(this ref Utf8JsonReader reader, JsonTokenType tokenType) - { - Assume(ref reader, tokenType); - reader.Read(); - } - } -} diff --git a/global.json b/global.json index 9dbbd4e163..8c70738ad6 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "8.0.301" + "version": "8.0.404" } } diff --git a/lang/IsExternalInit.cs b/lang/IsExternalInit.cs deleted file mode 100644 index 280baabc5c..0000000000 --- a/lang/IsExternalInit.cs +++ /dev/null @@ -1,23 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -// This file defines the IsExternalInit static class used to implement init-only properties. -// -// Documentation: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/init -// Source: https://github.com/dotnet/runtime/blob/v6.0.0/src/libraries/Common/src/System/Runtime/CompilerServices/IsExternalInit.cs - -#if NETSTANDARD2_0 - -using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; - -namespace System.Runtime.CompilerServices; - -[ExcludeFromCodeCoverage] -[EditorBrowsable(EditorBrowsableState.Never)] -internal static class IsExternalInit -{ } - -#endif diff --git a/release/scripts/PowerShell/DicomServerRelease/DicomServerRelease.psd1 b/release/scripts/PowerShell/DicomServerRelease/DicomServerRelease.psd1 deleted file mode 100644 index 3e2b5d4d43..0000000000 --- a/release/scripts/PowerShell/DicomServerRelease/DicomServerRelease.psd1 +++ /dev/null @@ -1,16 +0,0 @@ -# -# Module manifest for module 'DicomServerRelease' -# -@{ - RootModule = 'DicomServerRelease.psm1' - ModuleVersion = '0.0.1' - GUID = '4e840205-d0bd-4b83-9834-f799b4625355' - Author = 'Microsoft Healthcare NExT' - CompanyName = 'https://microsoft.com' - Description = 'PowerShell Module for managing Azure Active Directory registrations and users for Microsoft Dicom Server for a Test Environment. This module relies on the DicomServer module, and it must be imported before use of this module' - PowerShellVersion = '3.0' - FunctionsToExport = 'Add-AadTestAuthEnvironment', 'Remove-AadTestAuthEnvironment', 'Set-DicomServerApiApplicationRoles', 'Set-DicomServerClientAppRoleAssignments', 'Set-DicomServerUserAppRoleAssignments' - CmdletsToExport = @() - AliasesToExport = @() -} - \ No newline at end of file diff --git a/release/scripts/PowerShell/DicomServerRelease/DicomServerRelease.psm1 b/release/scripts/PowerShell/DicomServerRelease/DicomServerRelease.psm1 deleted file mode 100644 index 38debad9a3..0000000000 --- a/release/scripts/PowerShell/DicomServerRelease/DicomServerRelease.psm1 +++ /dev/null @@ -1,11 +0,0 @@ -$Public = @( Get-ChildItem -Path "$PSScriptRoot\Public\*.ps1" ) -$Private = @( Get-ChildItem -Path "$PSScriptRoot\Private\*.ps1" ) - -@($Public + $Private) | ForEach-Object { - Try { - . $_.FullName - } - Catch { - Write-Error -Message "Failed to import function $($_.FullName): $_" - } -} diff --git a/release/scripts/PowerShell/DicomServerRelease/Private/Grant-ClientAppAdminConsent.ps1 b/release/scripts/PowerShell/DicomServerRelease/Private/Grant-ClientAppAdminConsent.ps1 deleted file mode 100644 index 800ffad743..0000000000 --- a/release/scripts/PowerShell/DicomServerRelease/Private/Grant-ClientAppAdminConsent.ps1 +++ /dev/null @@ -1,166 +0,0 @@ -function Grant-ClientAppAdminConsent { - <# - .SYNOPSIS - Grants admin consent to a client app, so that users of the app are - not required to consent to the app calling the Dicom apli app on their behalf. - .PARAMETER AppId - The client application app ID. - .PARAMETER TenantAdminCredential - Credentials for a tenant admin user - #> - param( - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string]$AppId, - [Parameter(Mandatory = $true)] - [ValidateNotNull()] - [pscredential]$TenantAdminCredential - ) - - Set-StrictMode -Version Latest - - Write-Host "Granting admin consent for app ID $AppId" - - # get applicatioin and service principle - - $app = Get-AzureADApplication -Filter "AppId eq '$AppId'" - $sp = Get-AzureADServicePrincipal -Filter "AppId eq '$AppId'" - - foreach($access in $app.RequiredResourceAccess) - { - # grant permission for each required access - $targetAppId = $access.ResourceAppId - foreach($resourceAccess in $access.ResourceAccess) - { - # There are 2 types: Scope or Role - # Role refers to AppRole, can be granted via Graph API appRoleAssignments (https://docs.microsoft.com/en-us/graph/api/serviceprincipal-list-approleassignments?view=graph-rest-1.0&tabs=http) - # Scope refers to OAuth2Permission, also known as Delegated permission, can be granted via Graph API oauth2PermissionGrants (https://docs.microsoft.com/en-us/graph/api/oauth2permissiongrant-list?view=graph-rest-1.0&tabs=http) - # We currently don't have requirement for Role, so only handle Scope. - if($resourceAccess.Type -ne "Scope") - { - Write-Warning "Granting admin content on $($resourceAccess.Type) is not supported." - continue - } - $targetAppResourceId = $resourceAccess.Id - - # get target app service principle - $targetSp = Get-AzureADServicePrincipal -Filter "AppId eq '$targetAppId'" - - # get scope value - $oauth2Permission = $targetSp.Oauth2Permissions | ? {$_.Id -eq $targetAppResourceId} - $scopeValue = $oauth2Permission.Value - - # AllPrincipals indicates authorization to impersonate all users (https://docs.microsoft.com/en-us/graph/api/resources/oauth2permissiongrant?view=graph-rest-1.0#properties) - Grant-AzureAdOauth2Permission -ClientId $sp.ObjectId -ConsentType "AllPrincipals" -ResourceId $targetSp.ObjectId -Scope $scopeValue -TenantAdminCredential $TenantAdminCredential - Write-Host "Permission '$scopeValue' on '$($targetSp.appDisplayName)' to '$($sp.appDisplayName)' is granted!" - } - - } -} - -function Grant-AzureAdOauth2Permission -{ - param( - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string]$ClientId, - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string]$ConsentType, - [string]$PrincipalId, - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string]$ResourceId, - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string]$Scope, - [Parameter(Mandatory = $true)] - [ValidateNotNull()] - [pscredential]$TenantAdminCredential - ) - - # check existense - $existingEntry = Get-AzureADOAuth2PermissionGrant -All $true | ? {$_.ClientId -eq $sp.ObjectId -and $_.ResourceId -eq $targetSp.ObjectId -and $_.Scope -eq $Scope } - - if ($existingEntry) - { - # remove if exist - Remove-AzureADOAuth2PermissionGrant -ObjectId $existingEntry.ObjectId - } - Add-AzureAdOauth2PermissionGrant -ClientId $ClientId -ConsentType $ConsentType -PrincipalId $PrincipalId -ResourceId $ResourceId -Scope $Scope -TenantAdminCredential $TenantAdminCredential -} - - -function Get-GraphApiAccessToken -{ - param( - [Parameter(Mandatory = $true)] - [ValidateNotNull()] - [pscredential]$TenantAdminCredential - - ) - [string]$tenantId = ((Get-AzureADCurrentSessionInfo).Tenant.Id) - $username = $TenantAdminCredential.GetNetworkCredential().UserName - $password_raw = $TenantAdminCredential.GetNetworkCredential().Password - - $adTokenUrl = "https://login.microsoftonline.com/$tenantId/oauth2/token" - $resource = "https://graph.microsoft.com/" # microsoft graph Api - - $body = @{ - grant_type = "password" - username = $username - password = $password_raw - resource = $resource - client_id = "1950a258-227b-4e31-a9cf-717495945fc2" # Microsoft Azure PowerShell - } - $response = Invoke-RestMethod -Method 'Post' -Uri $adTokenUrl -ContentType "application/x-www-form-urlencoded" -Body $body - return $response.access_token - -} - -function Add-AzureAdOauth2PermissionGrant -{ - param( - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string]$ClientId, - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string]$ConsentType, - [string]$PrincipalId, - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string]$ResourceId, - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string]$Scope, - [Parameter(Mandatory = $true)] - [ValidateNotNull()] - [pscredential]$TenantAdminCredential - - ) - - [string]$tenantId = ((Get-AzureADCurrentSessionInfo).Tenant.Id) - - # get access token for Graph API - $accessToken = Get-GraphApiAccessToken -TenantAdminCredential $TenantAdminCredential - $body = @{ - clientId = $ClientId - consentType = $ConsentType - resourceId = $ResourceId - scope = $Scope - } - - if (-not [string]::IsNullOrEmpty($PrincipalId)) - { - $body.Add("principalId",$PrincipalId) - } - - $header = @{ - Authorization = "Bearer $accessToken" - 'Content-Type' = 'application/json' - } - - $response = Invoke-RestMethod "https://graph.microsoft.com/v1.0/oauth2PermissionGrants" -Method Post -Body ($body | ConvertTo-Json) -Headers $header - return $response -} \ No newline at end of file diff --git a/release/scripts/PowerShell/DicomServerRelease/Private/Set-DicomServerApiUsers.ps1 b/release/scripts/PowerShell/DicomServerRelease/Private/Set-DicomServerApiUsers.ps1 deleted file mode 100644 index 7a74872efc..0000000000 --- a/release/scripts/PowerShell/DicomServerRelease/Private/Set-DicomServerApiUsers.ps1 +++ /dev/null @@ -1,93 +0,0 @@ -function Set-DicomServerApiUsers { - <# - .SYNOPSIS - Configures (create/update) the needed users for the test environment. - .DESCRIPTION - .PARAMETER TenantDomain - The domain of the AAD tenant. - .PARAMETER ApiAppId - The AppId for the AAD application that contains the roles to be assigned. - .PARAMETER UserConfiguration - The collection of users from the testauthenvironment.json. - .PARAMETER UserNamePrefix - The prefix to use for the users to stop duplication/collision if multiple environments exist within the same AAD tenant. - .PARAMETER KeyVaultName - The name of the key vault to persist the user's passwords to. - #> - param( - [Parameter(Mandatory = $true )] - [ValidateNotNullOrEmpty()] - [string]$TenantDomain, - - [Parameter(Mandatory = $true )] - [ValidateNotNullOrEmpty()] - [string]$ApiAppId, - - [Parameter(Mandatory = $true )] - [ValidateNotNull()] - [object]$UserConfiguration, - - [Parameter(Mandatory = $false )] - [string]$UserNamePrefix, - - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string]$KeyVaultName - ) - - Set-StrictMode -Version Latest - - # Get current AzureAd context - try { - Get-AzureADCurrentSessionInfo -ErrorAction Stop | Out-Null - } - catch { - throw "Please log in to Azure AD with Connect-AzureAD cmdlet before proceeding" - } - - Write-Host "Persisting Users to AAD" - - $environmentUsers = @() - - foreach ($user in $UserConfiguration) { - $userId = $user.id - if ($UserNamePrefix) { - $userId = Get-UserId -EnvironmentName $UserNamePrefix -UserId $user.Id - } - - $userUpn = Get-UserUpn -UserId $userId -TenantDomain $TenantDomain - - # See if the user exists - $aadUser = Get-AzureADUser -Filter "userPrincipalName eq '$userUpn'" - - Add-Type -AssemblyName System.Web - $password = [System.Web.Security.Membership]::GeneratePassword(16, 5) - $passwordSecureString = ConvertTo-SecureString $password -AsPlainText -Force - - if ($aadUser) { - Set-AzureADUserPassword -ObjectId $aadUser.ObjectId -Password $passwordSecureString -EnforceChangePasswordPolicy $false -ForceChangePasswordNextLogin $false - } - else { - $PasswordProfile = New-Object -TypeName Microsoft.Open.AzureAD.Model.PasswordProfile - $PasswordProfile.Password = $password - $PasswordProfile.EnforceChangePasswordPolicy = $false - $PasswordProfile.ForceChangePasswordNextLogin = $false - - $aadUser = New-AzureADUser -DisplayName $userId -PasswordProfile $PasswordProfile -UserPrincipalName $userUpn -AccountEnabled $true -MailNickName $userId - } - - $upnSecureString = ConvertTo-SecureString -string $userUpn -AsPlainText -Force - Set-AzKeyVaultSecret -VaultName $KeyVaultName -Name "user--$($user.id)--id" -SecretValue $upnSecureString | Out-Null - Set-AzKeyVaultSecret -VaultName $KeyVaultName -Name "user--$($user.id)--secret" -SecretValue $passwordSecureString | Out-Null - - $environmentUsers += @{ - upn = $userUpn - environmentId = $userId - id = $user.id - } - - Set-DicomServerUserAppRoleAssignments -ApiAppId $ApiAppId -UserPrincipalName $userUpn -AppRoles $user.roles - } - - return $environmentUsers -} \ No newline at end of file diff --git a/release/scripts/PowerShell/DicomServerRelease/Private/SharedModuleFunctions.ps1 b/release/scripts/PowerShell/DicomServerRelease/Private/SharedModuleFunctions.ps1 deleted file mode 100644 index 43d402a7bd..0000000000 --- a/release/scripts/PowerShell/DicomServerRelease/Private/SharedModuleFunctions.ps1 +++ /dev/null @@ -1,112 +0,0 @@ -function Get-ApplicationDisplayName { - param ( - [Parameter(Mandatory = $false)] - [string]$EnvironmentName, - - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string]$AppId - ) - - if (!$EnvironmentName) { - return $AppId - } - else { - return "$EnvironmentName-$AppId" - } -} - -function Get-AzureAdApplicationByDisplayName { - param ( - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string]$DisplayName - ) - - Get-AzureAdApplication -Filter "DisplayName eq '$DisplayName'" -} - -function Get-AzureAdApplicationByIdentifierUri { - param ( - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string]$DicomServiceAudience - ) - - return Get-AzureAdApplication -Filter "identifierUris/any(uri:uri eq '$DicomServiceAudience')" -} - -function Get-AzureAdServicePrincipalByAppId { - param ( - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string]$AppId - ) - - return Get-AzureAdServicePrincipal -Filter "appId eq '$AppId'" -} - -function Get-ServiceAudience { - param ( - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string]$ServiceName, - - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string]$TenantIdDomain - ) - # AppId Uri in single tenant applications will require use of default scheme or verified domains - # It needs to be in one of the many formats mentioned in https://docs.microsoft.com/en-us/azure/active-directory/develop/reference-breaking-changes - # We use the format https://. - return "https://$ServiceName.$TenantIdDomain" -} - -function Get-UserId { - param ( - [Parameter(Mandatory = $false)] - [string]$EnvironmentName, - - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string]$UserId - ) - - if (!$EnvironmentName) { - return $UserId - } - else { - return "$EnvironmentName-$UserId" - } -} - -function Get-UserUpn { - param ( - [Parameter(Mandatory = $false)] - [string]$EnvironmentName, - - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string]$UserId, - - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string]$TenantDomain - ) - - return "$(Get-UserId -EnvironmentName $EnvironmentName -UserId $UserId)@$TenantDomain" -} - -function Get-AzureADAuthorityUri { - $aadEndpoint = (Get-AzureADCurrentSessionInfo).Environment.Endpoints["ActiveDirectory"] - $aadTenantId = (Get-AzureADCurrentSessionInfo).Tenant.Id.ToString() - "$aadEndpoint$aadTenantId" -} - -function Get-AzureADOpenIdConfiguration { - Invoke-WebRequest "$(Get-AzureADAuthorityUri)/.well-known/openid-configuration" | ConvertFrom-Json -} - -function Get-AzureADTokenEndpoint { - (Get-AzureADOpenIdConfiguration).token_endpoint -} diff --git a/release/scripts/PowerShell/DicomServerRelease/Public/Add-AadTestAuthEnvironment.ps1 b/release/scripts/PowerShell/DicomServerRelease/Public/Add-AadTestAuthEnvironment.ps1 deleted file mode 100644 index fa51f76f7c..0000000000 --- a/release/scripts/PowerShell/DicomServerRelease/Public/Add-AadTestAuthEnvironment.ps1 +++ /dev/null @@ -1,171 +0,0 @@ -function Add-AadTestAuthEnvironment { - <# - .SYNOPSIS - Adds all the required components for the test environment in AAD. - .DESCRIPTION - .PARAMETER TestAuthEnvironmentPath - Path for the testauthenvironment.json file - .PARAMETER EnvironmentName - Environment name used for the test environment. This is used throughout for making names unique. - .PARAMETER TenantAdminCredential - Credentials for a tenant admin user. Needed to grant admin consent to client apps. - .PARAMETER TenantIdDomain - TenantId domain ("*.onmicrosoft.com") used for creating service audience while creating AAD application. - #> - param - ( - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string]$TestAuthEnvironmentPath, - - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string]$EnvironmentName, - - [Parameter(Mandatory = $false)] - [string]$EnvironmentLocation = "West US", - - [Parameter(Mandatory = $true)] - [ValidateNotNull()] - [pscredential]$TenantAdminCredential, - - [Parameter(Mandatory = $true )] - [ValidateNotNullOrEmpty()] - [String]$TenantIdDomain, - - [Parameter(Mandatory = $false)] - [string]$ResourceGroupName = $EnvironmentName, - - [parameter(Mandatory = $false)] - [string]$KeyVaultName = "$EnvironmentName-ts" - ) - - Set-StrictMode -Version Latest - - # Get current AzureAd context - try { - $tenantInfo = Get-AzureADCurrentSessionInfo -ErrorAction Stop - } - catch { - throw "Please log in to Azure AD with Connect-AzureAD cmdlet before proceeding" - } - - # Get current Az context - try { - $azContext = Get-AzContext - } - catch { - throw "Please log in to Azure RM with Login-AzAccount cmdlet before proceeding" - } - - Write-Host "Setting up Test Authorization Environment for AAD" - - $testAuthEnvironment = Get-Content -Raw -Path $TestAuthEnvironmentPath | ConvertFrom-Json - - $keyVault = Get-AzKeyVault -VaultName $KeyVaultName - - if (!$keyVault) { - Write-Host "Creating keyvault with the name $KeyVaultName" - New-AzKeyVault -VaultName $KeyVaultName -ResourceGroupName $ResourceGroupName -Location $EnvironmentLocation | Out-Null - } - - $retryCount = 0 - # Make sure key vault exists and is ready - while (!(Get-AzKeyVault -VaultName $KeyVaultName )) { - $retryCount += 1 - - if ($retryCount -gt 7) { - throw "Could not connect to the vault $KeyVaultName" - } - - sleep 10 - } - - if ($azContext.Account.Type -eq "User") { - Write-Host "Current context is user: $($azContext.Account.Id)" - $currentObjectId = (Get-AzADUser -UserPrincipalName $azContext.Account.Id).Id - } - elseif ($azContext.Account.Type -eq "ServicePrincipal") { - Write-Host "Current context is service principal: $($azContext.Account.Id)" - $currentObjectId = (Get-AzADServicePrincipal -ServicePrincipalName $azContext.Account.Id).Id - } - else { - Write-Host "Current context is account of type '$($azContext.Account.Type)' with id of '$($azContext.Account.Id)" - throw "Running as an unsupported account type. Please use either a 'User' or 'Service Principal' to run this command" - } - - if ($currentObjectId) { - Write-Host "Adding permission to keyvault for $currentObjectId" - Set-AzKeyVaultAccessPolicy -VaultName $KeyVaultName -ObjectId $currentObjectId -PermissionsToSecrets Get,List,Set - } - - Write-Host "Ensuring API application exists" - - $dicomServiceAudience = Get-ServiceAudience -ServiceName $EnvironmentName -TenantIdDomain $TenantIdDomain - - $application = Get-AzureAdApplicationByIdentifierUri $dicomServiceAudience - - if (!$application) { - New-DicomServerApiApplicationRegistration -DicomServiceAudience $dicomServiceAudience - - # Change to use applicationId returned - $application = Get-AzureAdApplicationByIdentifierUri $dicomServiceAudience - } - - Write-Host "Setting roles on API Application" - $appRoles = ($testAuthEnvironment.users.roles + $testAuthEnvironment.clientApplications.roles) | Select-Object -Unique - Set-DicomServerApiApplicationRoles -ApiAppId $application.AppId -AppRoles $appRoles | Out-Null - - Write-Host "Ensuring users and role assignments for API Application exist (prefix: $($EnvironmentName))" - $environmentUsers = Set-DicomServerApiUsers -UserNamePrefix $EnvironmentName -TenantDomain $tenantInfo.TenantDomain -ApiAppId $application.AppId -UserConfiguration $testAuthEnvironment.Users -KeyVaultName $KeyVaultName - - $environmentClientApplications = @() - - Write-Host "Ensuring client application exists" - foreach ($clientApp in $testAuthEnvironment.clientApplications) { - $displayName = Get-ApplicationDisplayName -EnvironmentName $EnvironmentName -AppId $clientApp.Id - $aadClientApplication = Get-AzureAdApplicationByDisplayName $displayName - - if (!$aadClientApplication) { - - $aadClientApplication = New-DicomServerClientApplicationRegistration -ApiAppId $application.AppId -DisplayName "$displayName" -PublicClient:$true - - $secretSecureString = ConvertTo-SecureString $aadClientApplication.AppSecret -AsPlainText -Force - - } - else { - $existingPassword = Get-AzureADApplicationPasswordCredential -ObjectId $aadClientApplication.ObjectId | Remove-AzureADApplicationPasswordCredential -ObjectId $aadClientApplication.ObjectId - $newPassword = New-AzureADApplicationPasswordCredential -ObjectId $aadClientApplication.ObjectId - - $secretSecureString = ConvertTo-SecureString $newPassword.Value -AsPlainText -Force - } - - Grant-ClientAppAdminConsent -AppId $aadClientApplication.AppId -TenantAdminCredential $TenantAdminCredential - - $environmentClientApplications += @{ - id = $clientApp.Id - displayName = $displayName - appId = $aadClientApplication.AppId - } - - $appIdSecureString = ConvertTo-SecureString -String $aadClientApplication.AppId -AsPlainText -Force - Set-AzKeyVaultSecret -VaultName $KeyVaultName -Name "app--$($clientApp.Id)--id" -SecretValue $appIdSecureString | Out-Null - Set-AzKeyVaultSecret -VaultName $KeyVaultName -Name "app--$($clientApp.Id)--secret" -SecretValue $secretSecureString | Out-Null - - Set-DicomServerClientAppRoleAssignments -ApiAppId $application.AppId -AppId $aadClientApplication.AppId -AppRoles $clientApp.roles | Out-Null - } - - Write-Host "Set token and auth url in key vault" - $aadEndpoint = (Get-AzureADCurrentSessionInfo).Environment.Endpoints["ActiveDirectory"] - $aadTenantId = (Get-AzureADCurrentSessionInfo).Tenant.Id.ToString() - $tokenUrl = "$aadEndpoint$aadTenantId/oauth2/token" - $tokenUrlSecureString = ConvertTo-SecureString -String $tokenUrl -AsPlainText -Force - - Set-AzKeyVaultSecret -VaultName $KeyVaultName -Name "security--tokenUrl" -SecretValue $tokenUrlSecureString | Out-Null - - @{ - keyVaultName = $KeyVaultName - environmentUsers = $environmentUsers - environmentClientApplications = $environmentClientApplications - } -} diff --git a/release/scripts/PowerShell/DicomServerRelease/Public/Remove-AadTestAuthEnvironment.ps1 b/release/scripts/PowerShell/DicomServerRelease/Public/Remove-AadTestAuthEnvironment.ps1 deleted file mode 100644 index 8a7f36259d..0000000000 --- a/release/scripts/PowerShell/DicomServerRelease/Public/Remove-AadTestAuthEnvironment.ps1 +++ /dev/null @@ -1,70 +0,0 @@ -function Remove-AadTestAuthEnvironment { - <# - .SYNOPSIS - Removes the AAD components for the test environment in AAD. - .DESCRIPTION - .PARAMETER TestAuthEnvironmentPath - Path for the testauthenvironment.json file - .PARAMETER EnvironmentName - Environment name used for the test environment. This is used throughout for making names unique. - .PARAMETER TenantIdDomain - TenantId domain ("*.onmicrosoft.com") used for creating service audience while creating AAD application. - #> - param - ( - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string]$TestAuthEnvironmentPath, - - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string]$EnvironmentName, - - [Parameter(Mandatory = $true )] - [ValidateNotNullOrEmpty()] - [String]$TenantIdDomain - ) - - Set-StrictMode -Version Latest - - # Get current AzureAd context - try { - $tenantInfo = Get-AzureADCurrentSessionInfo -ErrorAction Stop - } - catch { - throw "Please log in to Azure AD with Connect-AzureAD cmdlet before proceeding" - } - - Write-Host "Tearing down test authorization environment for AAD" - - $testAuthEnvironment = Get-Content -Raw -Path $TestAuthEnvironmentPath | ConvertFrom-Json - - $dicomServiceAudience = Get-ServiceAudience -ServiceName $EnvironmentName -TenantIdDomain $TenantIdDomain - - $application = Get-AzureAdApplicationByIdentifierUri $dicomServiceAudience - - if ($application) { - Write-Host "Removing API application $dicomServiceAudience" - Remove-AzureAdApplication -ObjectId $application.ObjectId | Out-Null - } - - foreach ($user in $testAuthEnvironment.Users) { - $upn = Get-UserUpn -EnvironmentName $EnvironmentName -UserId $user.Id -TenantDomain $tenantInfo.TenantDomain - $aadUser = Get-AzureAdUser -Filter "userPrincipalName eq '$upn'" - - if ($aadUser) { - Write-Host "Removing user $upn" - Remove-AzureAdUser -ObjectId $aadUser.Objectid | Out-Null - } - } - - foreach ($clientApp in $testAuthEnvironment.ClientApplications) { - $displayName = Get-ApplicationDisplayName -EnvironmentName $EnvironmentName -AppId $clientApp.Id - $aadClientApplication = Get-AzureAdApplicationByDisplayName $displayName - - if ($aadClientApplication) { - Write-Host "Removing application $displayName" - Remove-AzureAdApplication -ObjectId $aadClientApplication.ObjectId | Out-Null - } - } -} diff --git a/release/scripts/PowerShell/DicomServerRelease/Public/Set-DicomServerApiApplicationRoles.ps1 b/release/scripts/PowerShell/DicomServerRelease/Public/Set-DicomServerApiApplicationRoles.ps1 deleted file mode 100644 index a47a878532..0000000000 --- a/release/scripts/PowerShell/DicomServerRelease/Public/Set-DicomServerApiApplicationRoles.ps1 +++ /dev/null @@ -1,89 +0,0 @@ -function Set-DicomServerApiApplicationRoles { - <# - .SYNOPSIS - Configures (create/update) the roles on the API application. - .DESCRIPTION - Configures (create/update) the roles of the API Application registration, specifically, it populates the AppRoles field of the application manifest. - .EXAMPLE - Set-DicomServerApiApplicationRoles -AppId -AppRoles globalReader,globalAdmin - .PARAMETER ApiAppId - ApiId for the API application - .PARAMETER AppRoles - List of roles to be defined on the API App - #> - param( - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string]$ApiAppId, - - [Parameter(Mandatory = $true)] - [ValidateNotNull()] - [string[]]$AppRoles - ) - - Set-StrictMode -Version Latest - - # Get current AzureAd context - try { - Get-AzureADCurrentSessionInfo -ErrorAction Stop | Out-Null - } - catch { - throw "Please log in to Azure AD with Connect-AzureAD cmdlet before proceeding" - } - - Write-Host "Persisting Roles to AAD application" - - $azureAdApplication = Get-AzureADApplication -Filter "AppId eq '$ApiAppId'" - - $appRolesToDisable = $false - $appRolesToEnable = $false - $desiredAppRoles = @() - - foreach ($role in $AppRoles) { - $existingAppRole = $azureAdApplication.AppRoles | Where-Object Value -eq $role - - if($existingAppRole) { - $id = $existingAppRole.Id - } - else { - $id = New-Guid - } - - $desiredAppRoles += @{ - AllowedMemberTypes = @("User", "Application") - Description = $role - DisplayName = $role - Id = $id - IsEnabled = "true" - Value = $role - } - } - - if (!($azureAdApplication.PsObject.Properties.Name -eq "AppRoles")) { - $appRolesToEnable = $true - } - else { - foreach ($diff in Compare-Object -ReferenceObject $desiredAppRoles -DifferenceObject $azureAdApplication.AppRoles -Property "Id") { - switch ($diff.SideIndicator) { - "<=" { - $appRolesToEnable = $true - } - "=>" { - ($azureAdApplication.AppRoles | Where-Object Id -eq $diff.Id).IsEnabled = $false - $appRolesToDisable = $true - } - } - } - } - - if ($appRolesToEnable -or $appRolesToDisable) { - if ($appRolesToDisable) { - Write-Host "Disabling old appRoles" - Set-AzureADApplication -ObjectId $azureAdApplication.objectId -appRoles $azureAdApplication.AppRoles | Out-Null - } - - # Update app roles - Write-Host "Updating appRoles" - Set-AzureADApplication -ObjectId $azureAdApplication.objectId -appRoles $desiredAppRoles | Out-Null - } -} \ No newline at end of file diff --git a/release/scripts/PowerShell/DicomServerRelease/Public/Set-DicomServerClientAppRoleAssignments.ps1 b/release/scripts/PowerShell/DicomServerRelease/Public/Set-DicomServerClientAppRoleAssignments.ps1 deleted file mode 100644 index f69e0add93..0000000000 --- a/release/scripts/PowerShell/DicomServerRelease/Public/Set-DicomServerClientAppRoleAssignments.ps1 +++ /dev/null @@ -1,113 +0,0 @@ -function Set-DicomServerClientAppRoleAssignments { - <# - .SYNOPSIS - Set app role assignments for the given client application - .DESCRIPTION - Set AppRoles for a given client application. Requires Azure AD admin privileges. - .EXAMPLE - Set-DicomServerClientAppRoleAssignments -AppId -ApiAppId -AppRoles globalReader,globalAdmin - .PARAMETER AppId - The AppId of the of the client application - .PARAMETER ApiAppId - The objectId of the API application that has roles that need to be assigned - .PARAMETER AppRoles - The collection of roles from the testauthenvironment.json for the client application - #> - param( - [Parameter(Mandatory = $true )] - [ValidateNotNullOrEmpty()] - [string]$AppId, - - [Parameter(Mandatory = $true )] - [ValidateNotNullOrEmpty()] - [string]$ApiAppId, - - [Parameter(Mandatory = $true )] - [AllowEmptyCollection()] - [string[]]$AppRoles - ) - - Set-StrictMode -Version Latest - - # Get current AzureAd context - try { - Get-AzureADCurrentSessionInfo -ErrorAction Stop | Out-Null - } - catch { - throw "Please log in to Azure AD with Connect-AzureAD cmdlet before proceeding" - } - - # Get the collection of roles for the user - $apiApplication = Get-AzureAdServicePrincipal -Filter "appId eq '$ApiAppId'" - $aadClientServicePrincipal = Get-AzureAdServicePrincipal -Filter "appId eq '$AppId'" - $ObjectId = $aadClientServicePrincipal.ObjectId - - Write-Host "API app: $apiApplication" - Write-Host "Client app: $aadClientServicePrincipal" - - $existingRoleAssignments = Get-AzureADServiceAppRoleAssignedTo -ObjectId $ObjectId | Where-Object {$_.ResourceId -eq $apiApplication.ObjectId} - - $expectedRoles = New-Object System.Collections.ArrayList - $rolesToAdd = New-Object System.Collections.ArrayList - $rolesToRemove = New-Object System.Collections.ArrayList - - foreach ($role in $AppRoles) { - $expectedRoles += @($apiApplication.AppRoles | Where-Object { $_.Value -eq $role }) - } - - foreach ($diff in Compare-Object -ReferenceObject @($expectedRoles | Select-Object) -DifferenceObject @($existingRoleAssignments | Select-Object) -Property "Id") { - switch ($diff.SideIndicator) { - "<=" { - $rolesToAdd += $diff.Id - } - "=>" { - $rolesToRemove += $diff.Id - } - } - } - - Write-Host "The following roles will be added: $rolesToAdd" - Write-Host "The following roles will be removed: $rolesToRemove" - - foreach ($role in $rolesToAdd) { - # This is known to report failure in certain scenarios, but will actually apply the permissions - try { - New-AzureADServiceAppRoleAssignment -ObjectId $ObjectId -PrincipalId $ObjectId -ResourceId $apiApplication.ObjectId -Id $role | Out-Null - } - catch { - #The role may have been assigned. Check: - $roleAssigned = Get-AzureADServiceAppRoleAssignedTo -ObjectId $ObjectId | Where-Object {$_.ResourceId -eq $apiApplication.ObjectId -and $_.Id -eq $role} - if (!$roleAssigned) { - throw "Failure adding app role assignment for service principal." - } - } - } - - foreach ($role in $rolesToRemove) { - Remove-AzureADServiceAppRoleAssignment -ObjectId $ObjectId -AppRoleAssignmentId ($existingRoleAssignments | Where-Object { $_.Id -eq $role }).ObjectId | Out-Null - } - - $finalRolesAssignments = Get-AzureADServiceAppRoleAssignment -ObjectId $apiApplication.ObjectId | Where-Object {$_.PrincipalId -eq $ObjectId} - $rolesNotAdded = $() - $rolesNotRemoved = $() - foreach ($diff in Compare-Object -ReferenceObject @($expectedRoles | Select-Object) -DifferenceObject @($finalRolesAssignments | Select-Object) -Property "Id") { - switch ($diff.SideIndicator) { - "<=" { - $rolesNotAdded += $diff.Id - } - "=>" { - $rolesNotRemoved += $diff.Id - } - } - } - - if($rolesNotAdded -or $rolesNotRemoved) { - if($rolesNotAdded) { - Write-Host "The following roles were not added: $rolesNotAdded" - } - - if($rolesNotRemoved) { - Write-Host "The following roles were not removed: $rolesNotRemoved" - } - } -} \ No newline at end of file diff --git a/release/scripts/PowerShell/DicomServerRelease/Public/Set-DicomServerUserAppRoleAssignments.ps1 b/release/scripts/PowerShell/DicomServerRelease/Public/Set-DicomServerUserAppRoleAssignments.ps1 deleted file mode 100644 index 1728c0b8c2..0000000000 --- a/release/scripts/PowerShell/DicomServerRelease/Public/Set-DicomServerUserAppRoleAssignments.ps1 +++ /dev/null @@ -1,77 +0,0 @@ -function Set-DicomServerUserAppRoleAssignments { - <# - .SYNOPSIS - Set app role assignments for a user - .DESCRIPTION - Set AppRoles for a given user. Requires Azure AD admin privileges. - .EXAMPLE - Set-DicomServerUserAppRoleAssignments -UserPrincipalName -ApiAppId -AppRoles globalReader,globalAdmin - .PARAMETER UserPrincipalName - The user principal name (e.g. myalias@contoso.com) of the of the user - .PARAMETER ApiAppId - The AppId of the API application that has roles that need to be assigned - .PARAMETER AppRoles - The array of roles for the client application - #> - param( - [Parameter(Mandatory = $true )] - [ValidateNotNullOrEmpty()] - [string]$UserPrincipalName, - - [Parameter(Mandatory = $true )] - [ValidateNotNullOrEmpty()] - [string]$ApiAppId, - - [Parameter(Mandatory = $true )] - [ValidateNotNull()] - [string[]]$AppRoles - ) - - Set-StrictMode -Version Latest - - # Get current AzureAd context - try { - Get-AzureADCurrentSessionInfo -ErrorAction Stop | Out-Null - } - catch { - throw "Please log in to Azure AD with Connect-AzureAD cmdlet before proceeding" - } - - $aadUser = Get-AzureADUser -Filter "UserPrincipalName eq '$UserPrincipalName'" - if (!$aadUser) - { - throw "User not found" - } - - $servicePrincipal = Get-AzureAdServicePrincipal -Filter "appId eq '$ApiAppId'" - - # Get the collection of roles for the user - $existingRoleAssignments = Get-AzureADUserAppRoleAssignment -ObjectId $aadUser.ObjectId | Where-Object {$_.ResourceId -eq $servicePrincipal.ObjectId} - - $expectedRoles = New-Object System.Collections.ArrayList - $rolesToAdd = New-Object System.Collections.ArrayList - $rolesToRemove = New-Object System.Collections.ArrayList - - foreach ($role in $AppRoles) { - $expectedRoles += @($servicePrincipal.AppRoles | Where-Object { $_.Value -eq $role }) - } - - foreach ($diff in Compare-Object -ReferenceObject @($expectedRoles | Select-Object) -DifferenceObject @($existingRoleAssignments | Select-Object) -Property "Id") { - switch ($diff.SideIndicator) { - "<=" { - $rolesToAdd += $diff.Id - } - "=>" { - $rolesToRemove += $diff.Id - } - } - } - - foreach ($role in $rolesToAdd) { - New-AzureADUserAppRoleAssignment -ObjectId $aadUser.ObjectId -PrincipalId $aadUser.ObjectId -ResourceId $servicePrincipal.ObjectId -Id $role | Out-Null - } - - foreach ($role in $rolesToRemove) { - Remove-AzureADUserAppRoleAssignment -ObjectId $aadUser.ObjectId -AppRoleAssignmentId ($existingRoleAssignments | Where-Object { $_.Id -eq $role }).ObjectId | Out-Null - } -} \ No newline at end of file diff --git a/release/templates/featuresenabled-azuredeploy.json b/release/templates/featuresenabled-azuredeploy.json deleted file mode 100644 index 2d3937beff..0000000000 --- a/release/templates/featuresenabled-azuredeploy.json +++ /dev/null @@ -1,180 +0,0 @@ -{ - "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "serviceName": { - "type": "string", - "minLength": 3, - "maxLength": 24, - "metadata": { - "description": "Name of the DICOM Service Web App." - } - }, - "appServicePlanResourceGroup": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Name of the resource group containing App Service Plan. If empty, your deployment resource group is used." - } - }, - "appServicePlanName": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Name of App Service Plan (existing or new). If empty, a name will be generated." - } - }, - "additionalDicomServerConfigProperties": { - "type": "object", - "defaultValue": {}, - "metadata": { - "description": "Additional configuration properties for the DICOM server. These properties can be modified after deployment. In the form {\"path1\":\"value1\",\"path2\":\"value2\"}" - } - }, - "solutionType": { - "type": "string", - "defaultValue": "DicomServerSqlServer", - "metadata": { - "description": "The type of the solution" - } - }, - "sqlAdminPassword": { - "type": "securestring", - "metadata": { - "description": "Set a password for the sql admin." - } - }, - "securityAuthenticationAuthority": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "OAuth Authority. This can be modified after deployment." - } - }, - "securityAuthenticationAudience": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Audience (aud) to validate in JWT. This can be modified after deployment." - } - } - }, - "variables": { - "loweredServiceName": "[toLower(parameters('serviceName'))]", - "featuresEnabledServiceName": "[concat(variables('loweredServiceName'),'-featuresenabled')]", - "appServicePlanResourceId": "[resourceId(parameters('appServicePlanResourceGroup'), 'Microsoft.Web/serverfarms/', parameters('appServicePlanName'))]", - "featuresEnabledAppServiceResourceId": "[resourceId('Microsoft.Web/sites', variables('featuresEnabledServiceName'))]", - "dicomDatabaseName": "DicomWithPartitions", - "staticDicomServerConfigProperties": { - "APPINSIGHTS_PORTALINFO": "ASP.NETCORE", - "APPINSIGHTS_PROFILERFEATURE_VERSION": "1.0.0", - "APPINSIGHTS_SNAPSHOTFEATURE_VERSION": "1.0.0", - "WEBSITE_NODE_DEFAULT_VERSION": "6.9.4", - "SqlServer__Initialize": "true", - "DicomFunctions__ConnectionName": "AzureWebJobsStorage", - "DicomServer__Security__Enabled": "true", - "DicomServer__Security__Authorization__Enabled": "true", - "DicomServer__Security__Authentication__Authority": "[parameters('securityAuthenticationAuthority')]", - "DicomServer__Security__Authentication__Audience": "[parameters('securityAuthenticationAudience')]", - "KeyVault__Endpoint": "[variables('keyVaultEndpoint')]" - }, - "keyVaultEndpoint": "[concat('https://', variables('loweredServiceName'), '.vault.azure.net/')]", - "combinedDicomServerConfigProperties": "[union(variables('staticDicomServerConfigProperties'), parameters('additionalDicomServerConfigProperties'))]", - "sqlServerResourceId": "[resourceId('Microsoft.Sql/servers/', variables('loweredServiceName'))]", - "sqlServerConnectionStringWithPartitionsName": "SqlServerConnectionStringWithPartitions", - "azureStorageConnectionStringName": "AzureStorageConnectionString", - "appInsightsInstrumentationKeyName": "AppInsightsInstrumentationKey", - "sqlServerConnectionStringWithPartitionsResourceId": "[resourceId('Microsoft.KeyVault/vaults/secrets', variables('loweredServiceName'), variables('sqlServerConnectionStringWithPartitionsName'))]", - "azureStorageConnectionStringResourceId": "[resourceId('Microsoft.KeyVault/vaults/secrets', variables('loweredServiceName'), variables('azureStorageConnectionStringName'))]", - "appInsightsInstrumentationKeyResourceId": "[resourceId('Microsoft.KeyVault/vaults/secrets', variables('loweredServiceName'), variables('appInsightsInstrumentationKeyName'))]" - }, - "resources": [ - { - "apiVersion": "2015-08-01", - "type": "Microsoft.Web/sites", - "name": "[variables('featuresEnabledServiceName')]", - "tags": { - "DicomServerSolution": "[parameters('solutionType')]" - }, - "location": "[resourceGroup().location]", - "identity": { - "type": "SystemAssigned" - }, - "properties": { - "clientAffinityEnabled": false, - "serverFarmId": "[variables('appServicePlanResourceId')]", - "siteConfig":{ - "use32BitWorkerProcess": false - } - }, - "resources": [ - { - "apiVersion": "2015-08-01", - "type": "config", - "name": "appsettings", - "properties": "[union(json(concat('{\"ApplicationInsights__InstrumentationKey\": \"', concat('@Microsoft.KeyVault(SecretUri=', reference(variables('appInsightsInstrumentationKeyResourceId'), '2015-06-01').secretUriWithVersion, ')'), '\"}')), json(concat('{\"AzureWebJobsStorage\": \"', concat('@Microsoft.KeyVault(SecretUri=', reference(variables('azureStorageConnectionStringResourceId'), '2015-06-01').secretUriWithVersion, ')'), '\"}')), json(concat('{\"BlobStore__ConnectionString\": \"', concat('@Microsoft.KeyVault(SecretUri=', reference(variables('azureStorageConnectionStringResourceId'), '2015-06-01').secretUriWithVersion, ')'), '\"}')), json(concat('{\"SqlServer__ConnectionString\": \"', concat('@Microsoft.KeyVault(SecretUri=', reference(variables('sqlServerConnectionStringWithPartitionsResourceId'), '2015-06-01').secretUriWithVersion, ')'), '\"}')), variables('combinedDicomServerConfigProperties'))]", - "dependsOn": [ - "[variables('featuresEnabledAppServiceResourceId')]" - ] - }, - { - "apiVersion": "2015-08-01", - "type": "siteextensions", - "name": "AspNetCoreRuntime.8.0.x64", - "dependsOn": [ - "[variables('featuresEnabledAppServiceResourceId')]", - "[resourceId('Microsoft.Web/sites/config', variables('featuresEnabledServiceName'), 'appsettings')]" // Avoid restarts mid-installation - ], - "properties": { - "version": "8.0.0" - } - } - ] - }, - { - "type": "Microsoft.Sql/servers/databases", - "apiVersion": "2017-10-01-preview", - "location": "[resourceGroup().location]", - "tags": { - "DicomServerSolution": "[parameters('solutionType')]" - }, - "name": "[concat(variables('loweredServiceName'), '/', variables('dicomDatabaseName'))]", - "properties": { - "collation": "SQL_Latin1_General_CP1_CI_AS" - }, - "sku": { - "name": "Standard", - "tier": "Standard", - "capacity": 20 - } - }, - { - "type": "Microsoft.KeyVault/vaults/accessPolicies", - "name": "[concat(variables('loweredServiceName'), '/add')]", - "apiVersion": "2019-09-01", - "properties": { - "accessPolicies": [ - { - "tenantId": "[reference(variables('featuresEnabledAppServiceResourceId'), '2015-08-01', 'Full').Identity.tenantId]", - "objectId": "[reference(variables('featuresEnabledAppServiceResourceId'), '2015-08-01', 'Full').Identity.principalId]", - "permissions": { - "secrets": [ "get" ] - } - } - ] - }, - "dependsOn": [ - "[variables('featuresEnabledAppServiceResourceId')]" - ] - }, - { - "type": "Microsoft.KeyVault/vaults/secrets", - "name": "[concat(variables('loweredServiceName'), '/', variables('sqlServerConnectionStringWithPartitionsName'))]", - "apiVersion": "2015-06-01", - "properties": { - "contentType": "text/plain", - "value": "[concat('Server=tcp:', reference(variables('sqlServerResourceId'), '2015-05-01-preview').fullyQualifiedDomainName,',1433;Initial Catalog=', variables('dicomDatabaseName'), ';Persist Security Info=False;User ID=', reference(variables('sqlServerResourceId'), '2015-05-01-preview').administratorLogin,';Password=',parameters('sqlAdminPassword'),';MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;')]" - } - } - ] -} diff --git a/samples/scripts/PowerShell/DicomServer.psd1 b/samples/scripts/PowerShell/DicomServer.psd1 deleted file mode 100644 index 0304c91c3e..0000000000 --- a/samples/scripts/PowerShell/DicomServer.psd1 +++ /dev/null @@ -1,16 +0,0 @@ -# -# Module manifest for module 'DicomServer' -# -@{ - RootModule = 'DicomServer.psm1' - ModuleVersion = '0.0.1' - GUID = '953E9CB7-49D5-448E-970C-E9E41BF60A1E' - Author = 'Microsoft Healthcare NExT' - CompanyName = 'https://microsoft.com' - Description = 'PowerShell Module for managing Azure Active Directory registrations and users for Microsoft Dicom Server.' - PowerShellVersion = '3.0' - FunctionsToExport = 'Remove-DicomServerApplicationRegistration', 'New-DicomServerClientApplicationRegistration', 'New-DicomServerApiApplicationRegistration' - CmdletsToExport = @() - AliasesToExport = @() -} - \ No newline at end of file diff --git a/samples/scripts/PowerShell/DicomServer.psm1 b/samples/scripts/PowerShell/DicomServer.psm1 deleted file mode 100644 index 9e197b4df5..0000000000 --- a/samples/scripts/PowerShell/DicomServer.psm1 +++ /dev/null @@ -1,9 +0,0 @@ -$Identity = @( Get-ChildItem -Path "$PSScriptRoot\Identity\*.ps1" ) - -@($Identity) | ForEach-Object { - Try { - . $_.FullName - } Catch { - Write-Error -Message "Failed to import function $($_.FullName): $_" - } -} diff --git a/samples/scripts/PowerShell/Identity/New-DicomServerApiApplicationRegistration.ps1 b/samples/scripts/PowerShell/Identity/New-DicomServerApiApplicationRegistration.ps1 deleted file mode 100644 index fe4c849b3a..0000000000 --- a/samples/scripts/PowerShell/Identity/New-DicomServerApiApplicationRegistration.ps1 +++ /dev/null @@ -1,81 +0,0 @@ -function New-DicomServerApiApplicationRegistration { - <# - .SYNOPSIS - Create an AAD Application registration for a Dicom server instance. - .DESCRIPTION - Create a new AAD Application registration for a Dicom server instance. - A DicomServiceName or DicomServiceAudience must be supplied. - .EXAMPLE - New-DicomServerApiApplicationRegistration -DicomServiceName "mydicomservice" -AppRoles admin,nurse - .EXAMPLE - New-DicomServerApiApplicationRegistration -DicomServiceAudience "https://mydicomservice.resoluteopensource.onmicrosoft.com" -AppRoles admin,nurse - .PARAMETER DicomServiceName - Name of the Dicom service instance. - .PARAMETER DicomServiceAudience - Full URL of the Dicom service. - .PARAMETER TenantIdDomain - TenantId domain ("*.onmicrosoft.com") used for creating service audience while creating AAD application. - .PARAMETER AppRoles - Names of AppRoles to be defined in the AAD Application registration - #> - [CmdletBinding(DefaultParameterSetName='ByDicomServiceName')] - param( - [Parameter(Mandatory = $true, ParameterSetName = 'ByDicomServiceName' )] - [ValidateNotNullOrEmpty()] - [string]$DicomServiceName, - - [Parameter(Mandatory = $true, ParameterSetName = 'ByDicomServiceAudience' )] - [ValidateNotNullOrEmpty()] - [string]$DicomServiceAudience, - - [Parameter(Mandatory = $true, ParameterSetName = 'ByDicomServiceName' )] - [ValidateNotNullOrEmpty()] - [String]$TenantIdDomain, - - [Parameter(Mandatory = $false)] - [String[]]$AppRoles = "admin" - ) - - Set-StrictMode -Version Latest - - # Get current AzureAd context - try { - Get-AzureADCurrentSessionInfo -ErrorAction Stop | Out-Null - } - catch { - throw "Please log in to Azure AD with Connect-AzureAD cmdlet before proceeding" - } - - if ([string]::IsNullOrEmpty($DicomServiceAudience)) { - $DicomServiceAudience = Get-ServiceAudience -ServiceName $DicomServiceName -TenantIdDomain $TenantIdDomain - } - - $desiredAppRoles = @() - foreach ($role in $AppRoles) { - $id = New-Guid - - $desiredAppRoles += @{ - AllowedMemberTypes = @("User", "Application") - Description = $role - DisplayName = $role - Id = $id - IsEnabled = "true" - Value = $role - } - } - - # Create the App Registration - $apiAppReg = New-AzureADApplication -DisplayName $DicomServiceAudience -IdentifierUris $DicomServiceAudience -AppRoles $desiredAppRoles - New-AzureAdServicePrincipal -AppId $apiAppReg.AppId | Out-Null - - $aadEndpoint = (Get-AzureADCurrentSessionInfo).Environment.Endpoints["ActiveDirectory"] - $aadTenantId = (Get-AzureADCurrentSessionInfo).Tenant.Id.ToString() - - #Return Object - @{ - AppId = $apiAppReg.AppId; - TenantId = $aadTenantId; - Authority = "$aadEndpoint$aadTenantId"; - Audience = $DicomServiceAudience; - } -} diff --git a/samples/scripts/PowerShell/Identity/New-DicomServerClientApplicationRegistration.ps1 b/samples/scripts/PowerShell/Identity/New-DicomServerClientApplicationRegistration.ps1 deleted file mode 100644 index 78724370fc..0000000000 --- a/samples/scripts/PowerShell/Identity/New-DicomServerClientApplicationRegistration.ps1 +++ /dev/null @@ -1,97 +0,0 @@ -function New-DicomServerClientApplicationRegistration { - <# - .SYNOPSIS - Create an AAD Application registration for a client application. - .DESCRIPTION - Create a new AAD Application registration for a client application that consumes an API. - .EXAMPLE - New-DicomServerClientApplicationRegistration -DisplayName "clientapplication" -ApiAppId 9125e524-1509-XXXX-XXXX-74137cc75422 - .PARAMETER ApiAppId - API AAD Application registration Id - .PARAMETER DisplayName - Display name for the client AAD Application registration - .PARAMETER ReplyUrl - Reply URL for the client AAD Application registration - .PARAMETER IdentifierUri - Identifier URI for the client AAD Application registration - .PARAMETER PublicClient - Switch to indicate if the client application should be a public client (desktop/mobile applications) - #> - param( - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string]$ApiAppId, - - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string]$DisplayName, - - [Parameter(Mandatory = $false)] - [string]$ReplyUrl = "https://www.getpostman.com/oauth2/callback", - - [Parameter(Mandatory = $false)] - [string]$IdentifierUri = "https://$DisplayName", - - [Parameter(Mandatory = $false)] - [switch]$PublicClient - ) - - Set-StrictMode -Version Latest - - # Get current AzureAd context - try { - Get-AzureADCurrentSessionInfo -ErrorAction Stop | Out-Null - } - catch { - throw "Please log in to Azure AD with Connect-AzureAD cmdlet before proceeding" - } - - $apiAppReg = Get-AzureADApplication -Filter "AppId eq '$ApiAppId'" - - # Some GUID values for Azure Active Directory - # https://blogs.msdn.microsoft.com/aaddevsup/2018/06/06/guid-table-for-windows-azure-active-directory-permissions/ - # Windows AAD Resource ID: - $windowsAadResourceId = "00000002-0000-0000-c000-000000000000" - # 'Sign in and read user profile' permission (scope) - $signInScope = "311a71cc-e848-46a1-bdf8-97ff7156d8e6" - - # Required App permission for Azure AD sign-in - $reqAad = New-Object -TypeName "Microsoft.Open.AzureAD.Model.RequiredResourceAccess" - $reqAad.ResourceAppId = $windowsAadResourceId - $reqAad.ResourceAccess = New-Object -TypeName "Microsoft.Open.AzureAD.Model.ResourceAccess" -ArgumentList $signInScope, "Scope" - - # Required App Permission for the API application registration. - $reqApi = New-Object -TypeName "Microsoft.Open.AzureAD.Model.RequiredResourceAccess" - $reqApi.ResourceAppId = $apiAppReg.AppId #From API App registration above - - # Just add the first scope (user impersonation) - $reqApi.ResourceAccess = New-Object -TypeName "Microsoft.Open.AzureAD.Model.ResourceAccess" -ArgumentList $apiAppReg.Oauth2Permissions[0].id, "Scope" - - if($PublicClient) - { - $clientAppReg = New-AzureADApplication -DisplayName $DisplayName -RequiredResourceAccess $reqAad, $reqApi -ReplyUrls $ReplyUrl -PublicClient $true - } - else - { - $clientAppReg = New-AzureADApplication -DisplayName $DisplayName -IdentifierUris $IdentifierUri -RequiredResourceAccess $reqAad, $reqApi -ReplyUrls $ReplyUrl - } - - # Create a client secret - $clientAppPassword = New-AzureADApplicationPasswordCredential -ObjectId $clientAppReg.ObjectId - - # Create Service Principal - New-AzureAdServicePrincipal -AppId $clientAppReg.AppId | Out-Null - - $securityAuthenticationAudience = $apiAppReg.IdentifierUris[0] - $aadEndpoint = (Get-AzureADCurrentSessionInfo).Environment.Endpoints["ActiveDirectory"] - $aadTenantId = (Get-AzureADCurrentSessionInfo).Tenant.Id.ToString() - $securityAuthenticationAuthority = "$aadEndpoint$aadTenantId" - - @{ - AppId = $clientAppReg.AppId; - AppSecret = $clientAppPassword.Value; - ReplyUrl = $clientAppReg.ReplyUrls[0] - AuthUrl = "$securityAuthenticationAuthority/oauth2/authorize?resource=$securityAuthenticationAudience" - TokenUrl = "$securityAuthenticationAuthority/oauth2/token" - } -} diff --git a/samples/scripts/PowerShell/Identity/Remove-DicomServerApplicationRegistration.ps1 b/samples/scripts/PowerShell/Identity/Remove-DicomServerApplicationRegistration.ps1 deleted file mode 100644 index bedd286366..0000000000 --- a/samples/scripts/PowerShell/Identity/Remove-DicomServerApplicationRegistration.ps1 +++ /dev/null @@ -1,49 +0,0 @@ -function Remove-DicomServerApplicationRegistration { - <# - .SYNOPSIS - Remove (delete) an AAD Application registration - .DESCRIPTION - Deletes an AAD Application registration with a specific AppId - .EXAMPLE - Remove-DicomServerApplicationRegistration -AppId 9125e524-1509-XXXX-XXXX-74137cc75422 - #> - [CmdletBinding(DefaultParameterSetName='ByIdentifierUri')] - param( - [Parameter(Mandatory = $true, ParameterSetName = 'ByAppId' )] - [ValidateNotNullOrEmpty()] - [string]$AppId, - - [Parameter(Mandatory = $true, ParameterSetName = 'ByIdentifierUri' )] - [ValidateNotNullOrEmpty()] - [string]$IdentifierUri - ) - - Set-StrictMode -Version Latest - - # Get current AzureAd context - try { - Get-AzureADCurrentSessionInfo -ErrorAction Stop - } - catch { - throw "Please log in to Azure AD with Connect-AzureAD cmdlet before proceeding" - } - - $appReg = $null - - if ($AppId) { - $appReg = Get-AzureADApplication -Filter "AppId eq '$AppId'" - if (!$appReg) { - Write-Host "Application with AppId = $AppId was not found." - return - } - } - else { - $appReg = Get-AzureADApplication -Filter "identifierUris/any(uri:uri eq '$IdentifierUri')" - if (!$appReg) { - Write-Host "Application with IdentifierUri = $IdentifierUri was not found." - return - } - } - - Remove-AzureADApplication -ObjectId $appReg.ObjectId -} diff --git a/samples/templates/Copy DICOM Metadata Changes to ADLS Gen2 in Delta Format.zip b/samples/templates/Copy DICOM Metadata Changes to ADLS Gen2 in Delta Format.zip deleted file mode 100644 index 79f921957d..0000000000 Binary files a/samples/templates/Copy DICOM Metadata Changes to ADLS Gen2 in Delta Format.zip and /dev/null differ diff --git a/samples/templates/default-azuredeploy.json b/samples/templates/default-azuredeploy.json deleted file mode 100644 index 5ce38e40a0..0000000000 --- a/samples/templates/default-azuredeploy.json +++ /dev/null @@ -1,585 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "serviceName": { - "type": "string", - "minLength": 3, - "maxLength": 24, - "metadata": { - "description": "Name of the DICOM Service Web App." - } - }, - "functionAppName": { - "type": "string", - "defaultValue": "", - "minLength": 0, - "maxLength": 60, - "metadata": { - "description": "Name of the DICOM Function App used for long-running operations." - } - }, - "location": { - "type": "string", - "allowedValues": [ - "australiaeast", - "brazilsouth", - "canadacentral", - "canadaeast", - "centralindia", - "centralus", - "eastasia", - "eastus", - "eastus2", - "francecentral", - "germanywestcentral", - "japaneast", - "koreacentral", - "koreasouth", - "northcentralus", - "northeurope", - "norwayeast", - "southafricanorth", - "southcentralus", - "southeastasia", - "uaenorth", - "uksouth", - "ukwest", - "westeurope", - "westindia", - "westus" - ], - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "The location of the Azure services used by the DICOM server." - } - }, - "appServicePlanSku": { - "type": "string", - "allowedValues": [ - "F1", - "D1", - "B1", - "B2", - "B3", - "S1", - "S2", - "S3", - "P1V2", - "P2V2", - "P3V2", - "P1V3", - "P2V3", - "P3V3" - ], - "defaultValue": "S1", - "metadata": { - "description": "Choose an App Service Plan SKU, or pricing tier. S1 is the default tier enabled." - } - }, - "storageAccountSku": { - "type": "string", - "allowedValues": [ - "Standard_LRS", - "Standard_GRS", - "Standard_RAGRS", - "Standard_ZRS", - "Premium_LRS", - "Premium_ZRS", - "Standard_GZRS", - "Standard_RAGZRS" - ], - "defaultValue": "Standard_LRS", - "metadata": { - "description": "Choose a SKU for your storage account. By default, Standard Locally Redundant Storage is selected." - } - }, - "sqlAdminPassword": { - "type": "securestring", - "metadata": { - "description": "Set a password for the sql admin." - } - }, - "appServicePlanName": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Name of App Service Plan (existing or new). If empty, a name will be generated." - } - }, - "appServicePlanResourceGroup": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Name of the resource group containing App Service Plan. If empty, your deployment resource group is used." - } - }, - "deployApplicationInsights": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Deploy Application Insights for the DICOM server." - } - }, - "additionalDicomServerConfigProperties": { - "type": "object", - "defaultValue": {}, - "metadata": { - "description": "Additional configuration properties for the DICOM server. These properties can be modified after deployment. In the form {\"path1\":\"value1\",\"path2\":\"value2\"}" - } - }, - "additionalDicomFunctionAppConfigProperties": { - "type": "object", - "defaultValue": {}, - "metadata": { - "description": "Additional configuration properties for the DICOM operations. These properties can be modified after deployment. In the form {\"path1\":\"value1\",\"path2\":\"value2\"}" - } - }, - "deployOhifViewer": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Deploy OHIF viewer that is configured for the DICOM server." - } - }, - "securityAuthenticationAuthority": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "OAuth Authority. This can be modified after deployment." - } - }, - "securityAuthenticationAudience": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Audience (aud) to validate in JWT. This can be modified after deployment." - } - }, - "solutionType": { - "type": "string", - "defaultValue": "DicomServerSqlServer", - "metadata": { - "description": "The type of the solution" - } - }, - "deployPackage": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Indicates whether the application code should be deployed along with the infrastructure." - } - }, - "deployWebPackageUrl": { - "type": "string", - "defaultValue": "https://dcmcistorage.blob.core.windows.net/cibuild/Microsoft.Health.Dicom.Web.zip", - "metadata": { - "description": "DICOM Server Webdeploy package to use as deployment code." - } - }, - "deployFunctionAppPackageUrl": { - "type": "string", - "defaultValue": "https://dcmcistorage.blob.core.windows.net/cibuild/Microsoft.Health.Dicom.Functions.App.zip", - "metadata": { - "description": "Azure Function App package to use as deployment code." - } - } - }, - "variables": { - "serviceName": "[toLower(parameters('serviceName'))]", - "functionAppName": "[if(empty(parameters('functionAppName')), concat(variables('serviceName'),'-functions'), toLower(parameters('functionAppName')))]", - "appServicePlanResourceGroup": "[if(empty(parameters('appServicePlanResourceGroup')), resourceGroup().name, parameters('appServicePlanResourceGroup'))]", - "appServicePlanName": "[if(empty(parameters('appServicePlanName')), concat(variables('serviceName'),'-asp'), parameters('appServicePlanName'))]", - "appServicePlanResourceId": "[resourceId(variables('appServicePlanResourceGroup'), 'Microsoft.Web/serverfarms/', variables('appServicePlanName'))]", - "appServiceResourceId": "[resourceId('Microsoft.Web/sites', variables('serviceName'))]", - "functionAppServiceResourceId": "[resourceId('Microsoft.Web/sites', variables('functionAppName'))]", - "appInsightsName": "[concat('AppInsights-', variables('serviceName'))]", - "securityAuthenticationEnabled": "[and(not(empty(parameters('securityAuthenticationAuthority'))),not(empty(parameters('securityAuthenticationAudience'))))]", - "staticDicomServerConfigProperties": { - "APPINSIGHTS_PORTALINFO": "ASP.NETCORE", - "APPINSIGHTS_PROFILERFEATURE_VERSION": "1.0.0", - "APPINSIGHTS_SNAPSHOTFEATURE_VERSION": "1.0.0", - "WEBSITE_NODE_DEFAULT_VERSION": "6.9.4", - "SqlServer__Initialize": "true", - "DicomFunctions__DurableTask__ConnectionName": "AzureWebJobsStorage", - "DicomServer__Features__EnableOhifViewer": "[parameters('deployOhifViewer')]", - "DicomServer__Security__Enabled": "[variables('securityAuthenticationEnabled')]", - "DicomServer__Security__Authentication__Authority": "[parameters('securityAuthenticationAuthority')]", - "DicomServer__Security__Authentication__Audience": "[parameters('securityAuthenticationAudience')]", - "KeyVault__VaultUri": "[variables('keyVaultEndpoint')]", - "KeyVault__Credential": "managedidentity" - }, - // See the following for Azure Function App settings: https://docs.microsoft.com/en-us/azure/azure-functions/functions-app-settings - "staticDicomFunctionAppConfigProperties": { - "AzureWebJobsDisableHomepage": "true", - "CONTAINER_NAME": "azure-webjobs-secrets", - "FUNCTIONS_EXTENSION_VERSION": "~4", - "FUNCTIONS_WORKER_RUNTIME": "dotnet", - "WEBSITE_ENABLE_SYNC_UPDATE_SITE": "1", - "AzureFunctionsJobHost__KeyVault__VaultUri": "[variables('keyVaultEndpoint')]", - "AzureFunctionsJobHost__KeyVault__Credential": "managedidentity" - }, - "combinedDicomServerConfigProperties": "[union(variables('staticDicomServerConfigProperties'), parameters('additionalDicomServerConfigProperties'))]", - "combinedDicomFunctionAppConfigProperties": "[union(variables('staticDicomFunctionAppConfigProperties'), parameters('additionalDicomFunctionAppConfigProperties'))]", - "sqlServerResourceId": "[resourceId('Microsoft.Sql/servers/', variables('serviceName'))]", - "dicomDatabaseName": "Dicom", - "keyVaultEndpoint": "[concat('https://', variables('serviceName'), '.vault.azure.net/')]", - "storageAccountName": "[concat(substring(replace(variables('serviceName'), '-', ''), 0, min(11, length(replace(variables('serviceName'), '-', '')))), uniquestring(resourceGroup().id))]", - "storageResourceId": "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]", - "keyVaultResourceId": "[resourceId('Microsoft.KeyVault/vaults', variables('serviceName'))]", - "sqlServerConnectionStringName": "SqlServerConnectionString", - "azureStorageConnectionStringName": "AzureStorageConnectionString", - "appInsightsInstrumentationKeyName": "AppInsightsInstrumentationKey", - "appInsightsConnectionStringName": "AppInsightsConnectionString", - "sqlServerConnectionStringResourceId": "[resourceId('Microsoft.KeyVault/vaults/secrets', variables('serviceName'), variables('sqlServerConnectionStringName'))]", - "azureStorageConnectionStringResourceId": "[resourceId('Microsoft.KeyVault/vaults/secrets', variables('serviceName'), variables('azureStorageConnectionStringName'))]", - "appInsightsInstrumentationKeyResourceId": "[resourceId('Microsoft.KeyVault/vaults/secrets', variables('serviceName'), variables('appInsightsInstrumentationKeyName'))]", - "appInsightsConnectionStringResourceId": "[resourceId('Microsoft.KeyVault/vaults/secrets', variables('serviceName'), variables('appInsightsConnectionStringName'))]" - }, - "resources": [ - { - "condition": "[empty(parameters('appServicePlanResourceGroup'))]", - "apiVersion": "2020-12-01", - "name": "[variables('appServicePlanName')]", - "type": "Microsoft.Web/serverfarms", - "tags": { - "DicomServerSolution": "[parameters('solutionType')]" - }, - "location": "[parameters('location')]", - "sku": { - "name": "[parameters('appServicePlanSku')]" - }, - "properties": { - "name": "[variables('appServicePlanName')]" - } - }, - { - "apiVersion": "2020-12-01", - "type": "Microsoft.Web/sites", - "name": "[variables('serviceName')]", - "tags": { - "DicomServerSolution": "[parameters('solutionType')]" - }, - "location": "[parameters('location')]", - "identity": { - "type": "SystemAssigned" - }, - "properties": { - "clientAffinityEnabled": false, - "serverFarmId": "[variables('appServicePlanResourceId')]", - "siteConfig": { - "location": "[parameters('location')]", - "use32BitWorkerProcess": false - } - }, - "dependsOn": [ - "[resourceId('Microsoft.Web/serverfarms/', variables('appServicePlanName'))]" - ], - "resources": [ - { - "apiVersion": "2020-12-01", - "type": "config", - "name": "appsettings", - "dependsOn": [ - "[variables('appServiceResourceId')]", - "[variables('sqlServerConnectionStringResourceId')]", - "[variables('azureStorageConnectionStringResourceId')]", - "[variables('appInsightsInstrumentationKeyResourceId')]", - "[variables('appInsightsConnectionStringResourceId')]" - ], - "properties": "[union(if(parameters('deployApplicationInsights'), json(concat('{\"APPINSIGHTS_INSTRUMENTATIONKEY\": \"', concat('@Microsoft.KeyVault(SecretUri=', reference(variables('appInsightsInstrumentationKeyResourceId')).secretUriWithVersion, ')'), '\" ,\"APPLICATIONINSIGHTS_CONNECTION_STRING\": \"', concat('@Microsoft.KeyVault(SecretUri=', reference(variables('appInsightsConnectionStringResourceId')).secretUriWithVersion, ')'), '\" ,\"ApplicationInsightsAgent_EXTENSION_VERSION\": \"~2\"}')), json('{}')), json(concat('{\"AzureWebJobsStorage\": \"', concat('@Microsoft.KeyVault(SecretUri=', reference(variables('azureStorageConnectionStringResourceId')).secretUriWithVersion, ')'), '\"}')), json(concat('{\"BlobStore__ConnectionString\": \"', concat('@Microsoft.KeyVault(SecretUri=', reference(variables('azureStorageConnectionStringResourceId')).secretUriWithVersion, ')'), '\"}')), json(concat('{\"SqlServer__ConnectionString\": \"', concat('@Microsoft.KeyVault(SecretUri=', reference(variables('sqlServerConnectionStringResourceId')).secretUriWithVersion, ')'), '\"}')), variables('combinedDicomServerConfigProperties'))]" - }, - { - "apiVersion": "2020-12-01", - "type": "siteextensions", - "name": "AspNetCoreRuntime.8.0.x64", - "dependsOn": [ - "[variables('appServiceResourceId')]", - "[resourceId('Microsoft.Web/sites/config', variables('serviceName'), 'appsettings')]" // Avoid restarts mid-installation - ], - "properties": { - "version": "8.0.0" - } - }, - { - "apiVersion": "2020-12-01", - "type": "extensions", - "name": "MSDeploy", - "condition": "[parameters('deployPackage')]", - "dependsOn": [ - "[variables('appServiceResourceId')]", - "[resourceId('Microsoft.Web/sites/config', variables('serviceName'), 'appsettings')]", - "[resourceId('Microsoft.Web/sites/siteextensions', variables('serviceName'), 'AspNetCoreRuntime.8.0.x64')]" - ], - "properties": { - "packageUri": "[parameters('deployWebPackageUrl')]" - } - } - ] - }, - { - "apiVersion": "2020-12-01", - "type": "Microsoft.Web/sites", - "name": "[variables('functionAppName')]", - "kind": "functionapp", - "tags": { - "DicomServerSolution": "[parameters('solutionType')]" - }, - "location": "[parameters('location')]", - "identity": { - "type": "SystemAssigned" - }, - "properties": { - "clientAffinityEnabled": false, - "serverFarmId": "[variables('appServicePlanResourceId')]", - "siteConfig": { - // We need to initially configure the Azure Function App for v3 so that it does not regenerate the function access keys later - "appSettings": [ - { - "name": "AzureWebJobsStorage", - "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(variables('storageResourceId'), '2021-09-01').keys[0].value, ';')]" - }, - { - "name": "CONTAINER_NAME", - "value": "azure-webjobs-secrets" - }, - { - "name": "FUNCTIONS_EXTENSION_VERSION", - "value": "~4" - }, - { - "name": "FUNCTIONS_WORKER_RUNTIME", - "value": "dotnet" - } - ], - "alwaysOn": true, - "location": "[parameters('location')]", - "use32BitWorkerProcess": false - } - }, - "dependsOn": [ - "[resourceId('Microsoft.Web/serverfarms/', variables('appServicePlanName'))]", - "[variables('storageResourceId')]" - ], - "resources": [ - { - "apiVersion": "2020-12-01", - "type": "config", - "name": "appsettings", - "dependsOn": [ - "[variables('functionAppServiceResourceId')]", - "[variables('sqlServerConnectionStringResourceId')]", - "[variables('azureStorageConnectionStringResourceId')]", - "[variables('appInsightsInstrumentationKeyResourceId')]", - "[variables('appInsightsConnectionStringResourceId')]" - ], - "properties": "[union(if(parameters('deployApplicationInsights'), json(concat('{\"APPINSIGHTS_INSTRUMENTATIONKEY\": \"', concat('@Microsoft.KeyVault(SecretUri=', reference(variables('appInsightsInstrumentationKeyResourceId')).secretUriWithVersion, ')'), '\" ,\"APPLICATIONINSIGHTS_CONNECTION_STRING\": \"', concat('@Microsoft.KeyVault(SecretUri=', reference(variables('appInsightsConnectionStringResourceId')).secretUriWithVersion, ')'), '\" ,\"ApplicationInsightsAgent_EXTENSION_VERSION\": \"~2\"}')), json('{}')), json(concat('{\"AzureFunctionsJobHost__BlobStore__ConnectionString\": \"', concat('@Microsoft.KeyVault(SecretUri=', reference(variables('azureStorageConnectionStringResourceId')).secretUriWithVersion, ')'), '\"}')), json(concat('{\"AzureFunctionsJobHost__SqlServer__ConnectionString\": \"', concat('@Microsoft.KeyVault(SecretUri=', reference(variables('sqlServerConnectionStringResourceId')).secretUriWithVersion, ')'), '\"}')), json(concat('{\"AzureWebJobsStorage\": \"', concat('@Microsoft.KeyVault(SecretUri=', reference(variables('azureStorageConnectionStringResourceId')).secretUriWithVersion, ')'), '\"}')), variables('combinedDicomFunctionAppConfigProperties'))]" - }, - { - "apiVersion": "2020-12-01", - "type": "extensions", - "name": "MSDeploy", - "condition": "[parameters('deployPackage')]", - "dependsOn": [ - "[variables('functionAppServiceResourceId')]", - "[resourceId('Microsoft.Web/sites/config', variables('functionAppName'), 'appsettings')]" - ], - "properties": { - "packageUri": "[parameters('deployFunctionAppPackageUrl')]" - } - } - ] - }, - { - "apiVersion": "2020-02-02", - "name": "[variables('appInsightsName')]", - "type": "Microsoft.Insights/components", - "location": "[parameters('location')]", - "condition": "[parameters('deployApplicationInsights')]", - "kind": "web", - "tags": { - "[concat('hidden-link:', variables('appServiceResourceId'))]": "Resource", - "displayName": "AppInsightsComponent", - "DicomServerSolution": "[parameters('solutionType')]" - }, - "properties": { - "Application_Type": "web" - } - }, - { - "name": "[variables('serviceName')]", - "type": "Microsoft.Sql/servers", - "apiVersion": "2021-08-01-preview", - "location": "[parameters('location')]", - "tags": { - "DicomServerSolution": "[parameters('solutionType')]" - }, - "properties": { - "administratorLogin": "dicomAdmin", - "administratorLoginPassword": "[parameters('sqlAdminPassword')]", - "version": "12.0" - }, - "resources": [ - { - "apiVersion": "2021-08-01-preview", - "dependsOn": [ - "[variables('serviceName')]" - ], - "location": "[parameters('location')]", - "tags": { - "DicomServerSolution": "[parameters('solutionType')]" - }, - "name": "[variables('dicomDatabaseName')]", - "properties": { - "collation": "SQL_Latin1_General_CP1_CI_AS" - }, - "sku": { - "name": "Standard", - "tier": "Standard", - "capacity": 20 - }, - "type": "databases" - }, - { - "apiVersion": "2021-08-01-preview", - "dependsOn": [ - "[variables('serviceName')]" - ], - "location": "[parameters('location')]", - "name": "AllowAllWindowsAzureIps", - "properties": { - "endIpAddress": "0.0.0.0", - "startIpAddress": "0.0.0.0" - }, - "type": "firewallrules" - } - ] - }, - { - "type": "Microsoft.Storage/storageAccounts", - "name": "[variables('storageAccountName')]", - "apiVersion": "2021-09-01", - "tags": { - "DicomServerSolution": "[parameters('solutionType')]" - }, - "location": "[parameters('location')]", - "kind": "StorageV2", - "sku": { - "name": "[parameters('storageAccountSku')]" - }, - "properties": { - "accessTier": "Hot", - "supportsHttpsTrafficOnly": true - } - }, - { - "type": "Microsoft.KeyVault/vaults", - "name": "[variables('serviceName')]", - "apiVersion": "2021-10-01", - "location": "[parameters('location')]", - "tags": { - "DicomServerSolution": "[parameters('solutionType')]" - }, - "properties": { - "sku": { - "family": "A", - "name": "Standard" - }, - "tenantId": "[subscription().tenantId]", - "accessPolicies": [ - { - "tenantId": "[reference(variables('appServiceResourceId'), '2015-08-01', 'Full').Identity.tenantId]", - "objectId": "[reference(variables('appServiceResourceId'), '2015-08-01', 'Full').Identity.principalId]", - "permissions": { - "secrets": [ - "delete", - "get", - "list", - "set" - ] - } - }, - { - "tenantId": "[reference(variables('functionAppServiceResourceId'), '2015-08-01', 'Full').Identity.tenantId]", - "objectId": "[reference(variables('functionAppServiceResourceId'), '2015-08-01', 'Full').Identity.principalId]", - "permissions": { - "secrets": [ - "delete", - "get", - "list", - "set" - ] - } - } - ], - "enabledForDeployment": false - }, - "dependsOn": [ - "[variables('appServiceResourceId')]", - "[variables('functionAppServiceResourceId')]" - ], - "resources": [ - { - "type": "secrets", - "name": "[variables('sqlServerConnectionStringName')]", - "apiVersion": "2021-10-01", - "properties": { - "contentType": "text/plain", - "value": "[concat('Server=tcp:', reference(variables('sqlServerResourceId'), '2015-05-01-preview').fullyQualifiedDomainName,',1433;Initial Catalog=', variables('dicomDatabaseName'), ';Persist Security Info=False;User ID=', reference(variables('sqlServerResourceId'), '2015-05-01-preview').administratorLogin,';Password=',parameters('sqlAdminPassword'),';MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;')]" - }, - "dependsOn": [ - "[variables('keyVaultResourceId')]", - "[variables('sqlServerResourceId')]" - ] - }, - { - "type": "secrets", - "name": "[variables('azureStorageConnectionStringName')]", - "apiVersion": "2021-10-01", - "properties": { - "contentType": "text/plain", - "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(variables('storageResourceId'), '2021-09-01').keys[0].value, ';')]" - }, - "dependsOn": [ - "[variables('keyVaultResourceId')]", - "[variables('storageResourceId')]" - ] - }, - { - "type": "secrets", - "name": "[variables('appInsightsInstrumentationKeyName')]", - "apiVersion": "2021-10-01", - "properties": { - "contentType": "text/plain", - "value": "[reference(resourceId('Microsoft.Insights/components/', variables('appInsightsName'))).instrumentationKey]" - }, - "condition": "[parameters('deployApplicationInsights')]", - "dependsOn": [ - "[variables('keyVaultResourceId')]", - "[resourceId('Microsoft.Insights/components/', variables('appInsightsName'))]" - ] - }, - { - "type": "secrets", - "name": "[variables('appInsightsConnectionStringName')]", - "apiVersion": "2021-10-01", - "properties": { - "contentType": "text/plain", - "value": "[reference(resourceId('Microsoft.Insights/components/', variables('appInsightsName'))).ConnectionString]" - }, - "condition": "[parameters('deployApplicationInsights')]", - "dependsOn": [ - "[variables('keyVaultResourceId')]", - "[resourceId('Microsoft.Insights/components/', variables('appInsightsName'))]" - ] - } - ] - } - ], - "outputs": { - "storageAccountName": { - "type": "string", - "value": "[variables('storageAccountName')]" - } - } -} diff --git a/samples/templates/dicomcast-fhir-dicom-azuredeploy.json b/samples/templates/dicomcast-fhir-dicom-azuredeploy.json deleted file mode 100644 index f1f979190e..0000000000 --- a/samples/templates/dicomcast-fhir-dicom-azuredeploy.json +++ /dev/null @@ -1,425 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "appServicePlanResourceGroup": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Name of the resource group containing App Service Plan. If empty, your deployment resource group is used." - } - }, - "appServicePlanName": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Name of App Service Plan (existing or new). If empty, a name will be generated." - } - }, - "appServicePlanSku": { - "type": "string", - "allowedValues": [ - "F1", - "D1", - "B1", - "B2", - "B3", - "S1", - "S2", - "S3", - "P1V2", - "P2V2", - "P3V2", - "P1V3", - "P2V3", - "P3V3" - ], - "defaultValue": "S1", - "metadata": { - "description": "Choose an App Service Plan SKU, or pricing tier. S1 is the default tier enabled." - } - }, - "dicomServiceName": { - "type": "string", - "minLength": 3, - "maxLength": 24, - "metadata": { - "description": "Name of the DICOM service Web App." - } - }, - "dicomStorageAccountSku": { - "type": "string", - "allowedValues": [ - "Standard_LRS", - "Standard_GRS", - "Standard_RAGRS", - "Standard_ZRS", - "Premium_LRS", - "Premium_ZRS", - "Standard_GZRS", - "Standard_RAGZRS" - ], - "defaultValue": "Standard_LRS", - "metadata": { - "description": "Choose a SKU for your storage account. By default, Standard Locally Redundant Storage is selected." - } - }, - "additionalDicomServerConfigProperties": { - "type": "object", - "defaultValue": {}, - "metadata": { - "description": "Additional configuration properties for the DICOM server. These properties can be modified after deployment. In the form {\"path1\":\"value1\",\"path2\":\"value2\"}" - } - }, - "dicomSqlAdminPassword": { - "type": "securestring", - "metadata": { - "description": "Set a password for the sql admin for DICOM." - } - }, - "deployOhifViewer": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Deploy OHIF viewer that is configured for the DICOM server." - } - }, - "dicomSecurityAuthenticationAuthority": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "OAuth Authority. This can be modified after deployment." - } - }, - "dicomSecurityAuthenticationAudience": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Audience (aud) to validate in JWT. This can be modified after deployment." - } - }, - "dicomSolutionType": { - "type": "string", - "defaultValue": "DicomServerSqlServer", - "metadata": { - "description": "The type of the solution" - } - }, - "dicomDeployPackages": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Webdeploy package specified by deployPackageUrl." - } - }, - "dicomDeployPackageUrl": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Webdeploy package to use as deployment code. If blank, the latest CI code package will be deployed." - } - }, - "functionDeployPackageUrl": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Azure Function App package to use as deployment code. If blank, the latest CI code package will be deployed." - } - }, - "fhirServiceName": { - "type": "string", - "minLength": 3, - "maxLength": 24, - "metadata": { - "description": "Name of the FHIR service Web App." - } - }, - "fhirSecurityAuthenticationAuthority": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "OAuth Authority" - } - }, - "fhirSecurityAuthenticationAudience": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Audience (aud) to validate in JWT" - } - }, - "enableAadSmartOnFhirProxy": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Enable Azure AAD SMART on FHIR Proxy" - } - }, - "msdeployPackageUrl": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Webdeploy package to use as depoyment code. If blank, the latest code package will be deployed." - } - }, - "additionalFhirServerConfigProperties": { - "type": "object", - "defaultValue": { - }, - "metadata": { - "description": "Additional configuration properties for the FHIR server. In the form {\"path1\":\"value1\",\"path2\":\"value2\"}" - } - }, - "fhirSqlAdminPassword": { - "type": "securestring", - "metadata": { - "description": "The password for the sql admin user if using SQL server." - } - }, - "fhirVersion": { - "type": "string", - "defaultValue": "R4", - "allowedValues": [ - "Stu3", - "R4", - "R5" - ], - "metadata": { - "description": "Only applies when MsdeployPackageUrl is not specified." - } - }, - "enableExport": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Determines whether export will be enabled for this fhir instance. If true, a storage account will be created as part of the deployment. You will need owner or user-administrator permissions for this." - } - }, - "enableConvertData": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Determines whether the $convert-data operation will be enabled for this fhir instance. If true, an Azure container registry will be created as part of the deployment. You will need owner or user-administrator permissions for this." - } - }, - "enableReindex": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Determines whether the $reindex operation will be enabled for this fhir instance." - } - }, - "dicomCastServiceName": { - "minLength": 3, - "maxLength": 24, - "type": "String", - "metadata": { - "description": "Name of the DICOM Cast service container group." - } - }, - "image": { - "defaultValue": "dicomoss.azurecr.io/dicom-cast", - "type": "String", - "metadata": { - "description": "Container image to deploy. Should be of the form repoName/imagename for images stored in public Docker Hub, or a fully qualified URI for other registries. Images from private registries require additional registry credentials." - } - }, - "imageTag": { - "type": "String", - "metadata": { - "description": "Image tag. Ex: 10.0.479. You can find the latest https://github.com/microsoft/dicom-server/tags" - } - }, - "dicomCastStorageAccountSku": { - "defaultValue": "Standard_LRS", - "allowedValues": [ - "Standard_LRS", - "Standard_GRS", - "Standard_RAGRS", - "Standard_ZRS", - "Premium_LRS", - "Premium_ZRS", - "Standard_GZRS", - "Standard_RAGZRS" - ], - "type": "String" - }, - "dicomCastCpuCores": { - "defaultValue": "1.0", - "type": "String", - "metadata": { - "description": "The number of CPU cores to allocate to the container." - } - }, - "dicomCastMemoryInGb": { - "defaultValue": "1.5", - "type": "String", - "metadata": { - "description": "The amount of memory to allocate to the container in gigabytes." - } - }, - "dicomCastRestartPolicy": { - "defaultValue": "always", - "allowedValues": [ - "never", - "always", - "onfailure" - ], - "type": "String", - "metadata": { - "description": "The behavior of Azure runtime if container has stopped." - } - }, - "deployApplicationInsights": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Deploy Application Insights for the DICOM server, FHIR Server and DICOM Cast. Disabled for Microsoft Azure Government (MAG)" - } - }, - "applicationInsightsLocation": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "allowedValues": [ - "southeastasia", - "northeurope", - "westeurope", - "eastus", - "southcentralus", - "westus2" - ], - "metadata": { - "description": "Select a location for Application Insights. If empty, the region closet to your deployment location is used." - } - }, - "enforceValidationOfTagValues": { - "defaultValue": false, - "type": "Bool", - "metadata": { - "description": "Enforce validation of all tag values when syncing from Dicom to FHIR using dicom-cast and do not store to FHIR even if only non-required tags are invalid" - } - }, - "ignoreJsonParsingErrors": { - "defaultValue": false, - "type": "Bool", - "metadata": { - "description": "Ignore json parsing errors for DICOM instances with malformed DICOM json" - } - } - }, - "variables": { - "appServicePlanResourceGroup": "[if(empty(parameters('appServicePlanResourceGroup')), resourceGroup().name, parameters('appServicePlanResourceGroup'))]", - "appServicePlanName": "[if(empty(parameters('appServicePlanName')), concat(parameters('dicomServiceName'),'-asp'), parameters('appServicePlanName'))]" - }, - "resources": [ - { - "condition": "[empty(parameters('appServicePlanResourceGroup'))]", - "apiVersion": "2015-08-01", - "name": "[variables('appServicePlanName')]", - "type": "Microsoft.Web/serverfarms", - "tags": { - "DicomServerSolution": "[parameters('dicomSolutionType')]" - }, - "location": "[resourceGroup().location]", - "sku": { - "name": "[parameters('appServicePlanSku')]" - }, - "properties": { - "name": "[variables('appServicePlanName')]" - } - }, - { - "type": "Microsoft.Resources/deployments", - "apiVersion": "2015-01-01", - "name": "dicomLinkedTemplate", - "properties": { - "mode": "Incremental", - "templateLink": { - "uri": "https://raw.githubusercontent.com/microsoft/dicom-server/main/samples/templates/default-azuredeploy.json", - "contentVersion": "1.0.0.0" - }, - "parameters": { - "serviceName": { "value": "[parameters('dicomServiceName')]" }, - "appServicePlanResourceGroup": { "value": "[variables('appServicePlanResourceGroup')]" }, - "appServicePlanName": { "value": "[variables('appServicePlanName')]" }, - "appServicePlanSku": { "value": "[parameters('appServicePlanSku')]" }, - "storageAccountSku": { "value": "[parameters('dicomStorageAccountSku')]" }, - "deployApplicationInsights": { "value": "[parameters('deployApplicationInsights')]" }, - "applicationInsightsLocation": { "value": "[parameters('applicationInsightsLocation')]" }, - "additionalDicomServerConfigProperties": { "value": "[parameters('additionalDicomServerConfigProperties')]" }, - "sqlAdminPassword": { "value": "[parameters('dicomSqlAdminPassword')]" }, - "sqlLocation": { "value": "[resourceGroup().location]" }, - "deployOhifViewer": { "value": "[parameters('deployOhifViewer')]" }, - "securityAuthenticationAuthority": { "value": "[parameters('dicomSecurityAuthenticationAuthority')]" }, - "securityAuthenticationAudience": { "value": "[parameters('dicomSecurityAuthenticationAudience')]" }, - "solutionType": { "value": "[parameters('dicomSolutionType')]" }, - "deployPackage": { "value": "[parameters('dicomDeployPackages')]" }, - "deployWebPackageUrl": { "value": "[parameters('dicomDeployPackageUrl')]" }, - "deployFunctionAppPackageUrl": { "value": "[parameters('functionDeployPackageUrl')]" } - } - } - }, - { - "type": "Microsoft.Resources/deployments", - "apiVersion": "2015-01-01", - "name": "fhirLinkedTemplate", - "properties": { - "mode": "Incremental", - "templateLink": { - "uri": "https://raw.githubusercontent.com/microsoft/fhir-server/main/samples/templates/default-azuredeploy.json", - "contentVersion": "1.0.0.0" - }, - "parameters": { - "serviceName": { "value": "[parameters('fhirServiceName')]" }, - "appServicePlanResourceGroup": { "value": "[variables('appServicePlanResourceGroup')]" }, - "appServicePlanName": { "value": "[variables('appServicePlanName')]" }, - "appServicePlanSku": { "value": "[parameters('appServicePlanSku')]" }, - "securityAuthenticationAuthority": { "value": "[parameters('fhirSecurityAuthenticationAuthority')]" }, - "securityAuthenticationAudience": { "value": "[parameters('fhirSecurityAuthenticationAudience')]" }, - "enableAadSmartOnFhirProxy": { "value": "[parameters('enableAadSmartOnFhirProxy')]" }, - "msdeployPackageUrl": { "value": "[parameters('msdeployPackageUrl')]" }, - "deployApplicationInsights": { "value": "[parameters('deployApplicationInsights')]" }, - "applicationInsightsLocation": { "value": "[parameters('applicationInsightsLocation')]" }, - "additionalFhirServerConfigProperties": { "value": "[parameters('additionalFhirServerConfigProperties')]" }, - "solutionType": { "value": "FhirServerSqlServer" }, - "sqlAdminPassword": { "value": "[parameters('fhirSqlAdminPassword')]" }, - "sqlLocation": { "value": "[resourceGroup().location]" }, - "fhirVersion": { "value": "[parameters('fhirVersion')]" }, - "enableExport": { "value": "[parameters('enableExport')]" }, - "enableConvertData": { "value": "[parameters('enableConvertData')]" }, - "enableReindex": { "value": "[parameters('enableReindex')]" }, - "sqlSchemaAutomaticUpdatesEnabled": { "value": "auto" } - } - } - }, - { - "type": "Microsoft.Resources/deployments", - "apiVersion": "2015-01-01", - "name": "dicomCastLinkedTemplate", - "properties": { - "mode": "Incremental", - "templateLink": { - "uri": "https://raw.githubusercontent.com/microsoft/dicom-server/main/converter/dicom-cast/samples/templates/default-azuredeploy.json", - "contentVersion": "1.0.0.0" - }, - "parameters": { - "serviceName": { "value": "[parameters('dicomCastServiceName')]" }, - "image": { "value": "[concat(parameters('image'), ':', parameters('imageTag'))]" }, - "storageAccountSku": { "value": "[parameters('dicomCastStorageAccountSku')]" }, - "deployApplicationInsights": { "value": "[parameters('deployApplicationInsights')]" }, - "applicationInsightsLocation": { "value": "[parameters('applicationInsightsLocation')]" }, - "cpuCores": { "value": "[parameters('dicomCastCpuCores')]" }, - "memoryInGb": { "value": "[parameters('dicomCastMemoryInGb')]" }, - "location": { "value": "[resourceGroup().location]" }, - "restartPolicy": { "value": "[parameters('dicomCastRestartPolicy')]" }, - "dicomWebEndpoint": { "value": "[concat('https://',parameters('dicomServiceName'),'.azurewebsites.net')]" }, - "fhirEndpoint": { "value": "[concat('https://',parameters('fhirServiceName'),'.azurewebsites.net')]" }, - "enforceValidationOfTagValues": { "value": "[parameters('enforceValidationOfTagValues')]" }, - "ignoreJsonParsingErrors": { "value": "[parameters('ignoreJsonParsingErrors')]" } - } - } - } - ], - "outputs": { - } -} \ No newline at end of file diff --git a/samples/templates/dicomcast-quick-deploy.json b/samples/templates/dicomcast-quick-deploy.json deleted file mode 100644 index cdb70c841e..0000000000 --- a/samples/templates/dicomcast-quick-deploy.json +++ /dev/null @@ -1,155 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "serviceName": { - "type": "string", - "minLength": 6, - "maxLength": 18, - "metadata": { - "description": "Base name of service for DICOM, FHIR and DICOM Cast." - } - }, - "sqlAdminPassword": { - "type": "securestring", - "metadata": { - "description": "Set a password for the sql admin for DICOM and FHIR." - } - }, - "patientSystemId": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Patient SystemId configured by the user" - } - }, - "isIssuerIdUsed": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Issuer id or patient system id used based on this boolean value" - } - }, - "image": { - "defaultValue": "dicomoss.azurecr.io/dicom-cast", - "type": "String", - "metadata": { - "description": "Container image to deploy. Should be of the form repoName/imagename for images stored in public Docker Hub, or a fully qualified URI for other registries. Images from private registries require additional registry credentials." - } - }, - "imageTag": { - "type": "String", - "metadata": { - "description": "Image tag. Ex: 10.0.479. You can find the latest https://github.com/microsoft/dicom-server/tags" - } - } - }, - "variables": { - "appServicePlanResourceGroup": "[resourceGroup().name]", - "appServicePlanName": "[concat(parameters('serviceName'),'-asp')]", - "appServicePlanResourceId": "[resourceId(variables('appServicePlanResourceGroup'), 'Microsoft.Web/serverfarms/', variables('appServicePlanName'))]" - }, - "resources": [ - { - "apiVersion": "2015-08-01", - "name": "[variables('appServicePlanName')]", - "type": "Microsoft.Web/serverfarms", - "tags": { - "DicomServerSolution": "DicomServerSqlServer" - }, - "location": "[resourceGroup().location]", - "sku": { - "name": "S1" - }, - "properties": { - "name": "[variables('appServicePlanName')]" - } - }, - { - "type": "Microsoft.Resources/deployments", - "apiVersion": "2015-01-01", - "name": "dicomLinkedTemplate", - "properties": { - "mode": "Incremental", - "templateLink": { - "uri":"https://raw.githubusercontent.com/microsoft/dicom-server/main/samples/templates/default-azuredeploy.json", - "contentVersion":"1.0.0.0" - }, - "parameters":{ - "serviceName":{"value": "[concat(parameters('serviceName'),'-dicom')]"}, - "appServicePlanResourceGroup":{"value": "[variables('appServicePlanResourceGroup')]"}, - "appServicePlanName":{"value": "[variables('appServicePlanName')]"}, - "appServicePlanSku":{"value": "S1"}, - "storageAccountSku":{"value": "Standard_LRS"}, - "deployApplicationInsights":{"value": true}, - "applicationInsightsLocation":{"value": "[resourceGroup().location]"}, - "sqlAdminPassword":{"value": "[parameters('sqlAdminPassword')]"}, - "sqlLocation":{"value": "[resourceGroup().location]"}, - "deployOhifViewer":{"value": true}, - "solutionType":{"value": "DicomServerSqlServer"}, - "deployPackage":{"value": true} - } - } - }, - { - "type": "Microsoft.Resources/deployments", - "apiVersion": "2015-01-01", - "name": "fhirLinkedTemplate", - "properties": { - "mode": "Incremental", - "templateLink": { - "uri":"https://raw.githubusercontent.com/microsoft/fhir-server/main/samples/templates/default-azuredeploy.json", - "contentVersion":"1.0.0.0" - }, - "parameters":{ - "serviceName":{"value": "[concat(parameters('serviceName'),'-fhir')]"}, - "appServicePlanResourceGroup":{"value": "[variables('appServicePlanResourceGroup')]"}, - "appServicePlanName":{"value": "[variables('appServicePlanName')]"}, - "appServicePlanSku":{"value": "S1"}, - "enableAadSmartOnFhirProxy":{"value": false}, - "deployApplicationInsights":{"value": true}, - "applicationInsightsLocation":{"value": "[resourceGroup().location]"}, - "solutionType":{"value": "FhirServerSqlServer"}, - "sqlAdminPassword":{"value": "[parameters('sqlAdminPassword')]"}, - "sqlLocation":{"value": "[resourceGroup().location]"}, - "fhirVersion":{"value": "R4"}, - "enableExport":{"value": false}, - "enableConvertData":{"value": false}, - "enableReindex":{"value": false}, - "sqlSchemaAutomaticUpdatesEnabled":{"value": "auto"} - } - } - }, - { - "type": "Microsoft.Resources/deployments", - "apiVersion": "2015-01-01", - "name": "dicomCastLinkedTemplate", - "properties": { - "mode": "Incremental", - "templateLink": { - "uri":"https://raw.githubusercontent.com/microsoft/dicom-server/main/converter/dicom-cast/samples/templates/default-azuredeploy.json", - "contentVersion":"1.0.0.0" - }, - "parameters":{ - "serviceName":{"value": "[concat(parameters('serviceName'),'-dcast')]"}, - "image":{"value": "[concat(parameters('image'), ':', parameters('imageTag'))]" }, - "storageAccountSku":{"value": "Standard_LRS"}, - "deployApplicationInsights":{"value": true}, - "applicationInsightsLocation":{"value": "[resourceGroup().location]"}, - "cpuCores":{"value": "1.0"}, - "memoryInGb":{"value": "1.5"}, - "location":{"value": "[resourceGroup().location]"}, - "restartPolicy":{"value": "always"}, - "dicomWebEndpoint":{"value": "[concat('https://',parameters('serviceName'), '-dicom','.azurewebsites.net')]"}, - "fhirEndpoint":{"value": "[concat('https://',parameters('serviceName'),'-fhir','.azurewebsites.net')]"}, - "patientSystemId": { "value": "[parameters('patientSystemId')]" }, - "isIssuerIdUsed": { "value": "[parameters('isIssuerIdUsed')]" }, - "enforceValidationOfTagValues":{"value": false}, - "ignoreJsonParsingErrors": {"value": false} - } - } - } - ], - "outputs": { - } - } diff --git a/src/Microsoft.Health.Dicom.Api.UnitTests/Controllers/ChangeFeedControllerTests.cs b/src/Microsoft.Health.Dicom.Api.UnitTests/Controllers/ChangeFeedControllerTests.cs deleted file mode 100644 index a911bb926d..0000000000 --- a/src/Microsoft.Health.Dicom.Api.UnitTests/Controllers/ChangeFeedControllerTests.cs +++ /dev/null @@ -1,231 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Threading.Tasks; -using MediatR; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Versioning; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Health.Dicom.Api.Controllers; -using Microsoft.Health.Dicom.Api.Models; -using Microsoft.Health.Dicom.Core.Features.ChangeFeed; -using Microsoft.Health.Dicom.Core.Messages.ChangeFeed; -using Microsoft.Health.Dicom.Core.Models; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Api.UnitTests.Controllers; - -public class ChangeFeedControllerTests -{ - private readonly ChangeFeedController _controller; - private readonly IMediator _mediator = Substitute.For(); - - public ChangeFeedControllerTests() - { - _controller = new ChangeFeedController(_mediator, NullLogger.Instance) - { - ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() }, - }; - } - - [Fact] - public async Task GivenNoUserValues_WhenFetchingChangeFeed_ThenUseDefaultValues() - { - var expected = new List(); - - _mediator - .Send( - Arg.Is(x => - x.Range == TimeRange.MaxValue && - x.Offset == 0L && - x.Limit == 10 && - x.IncludeMetadata), - _controller.HttpContext.RequestAborted) - .Returns(new ChangeFeedResponse(expected)); - - IActionResult result = await _controller.GetChangeFeedAsync(); - var actual = result as ObjectResult; - Assert.Same(expected, actual.Value); - - await _mediator - .Received(1) - .Send( - Arg.Is(x => - x.Range == TimeRange.MaxValue && - x.Offset == 0L && - x.Limit == 10 && - x.IncludeMetadata), - _controller.HttpContext.RequestAborted); - } - - [Theory] - [InlineData(0, 5, false)] - [InlineData(int.MaxValue + 1L, 25, true)] - public async Task GivenParameters_WhenFetchingChangeFeed_ThenPassValues(long offset, int limit, bool includeMetadata) - { - var expected = new List(); - - _mediator - .Send( - Arg.Is(x => - x.Range == TimeRange.MaxValue && - x.Offset == offset && - x.Limit == limit && - x.Order == ChangeFeedOrder.Sequence && - x.IncludeMetadata == includeMetadata), - _controller.HttpContext.RequestAborted) - .Returns(new ChangeFeedResponse(expected)); - - IActionResult result = await _controller.GetChangeFeedAsync(offset, limit, includeMetadata); - var actual = result as ObjectResult; - Assert.Same(expected, actual.Value); - - await _mediator - .Received(1) - .Send( - Arg.Is(x => - x.Range == TimeRange.MaxValue && - x.Offset == offset && - x.Limit == limit && - x.Order == ChangeFeedOrder.Sequence && - x.IncludeMetadata == includeMetadata), - _controller.HttpContext.RequestAborted); - } - - [Fact] - public async Task GivenNoUserValues_WhenFetchingWindowedChangeFeed_ThenUseProperDefaults() - { - var expected = new List(); - - _mediator - .Send( - Arg.Is(x => - x.Range == TimeRange.MaxValue && - x.Offset == 0L && - x.Limit == 100 && - x.IncludeMetadata), - _controller.HttpContext.RequestAborted) - .Returns(new ChangeFeedResponse(expected)); - - IActionResult result = await _controller.GetChangeFeedAsync(new WindowedPaginationOptions()); - var actual = result as ObjectResult; - Assert.Same(expected, actual.Value); - - await _mediator - .Received(1) - .Send( - Arg.Is(x => - x.Range == TimeRange.MaxValue && - x.Offset == 0L && - x.Limit == 100 && - x.IncludeMetadata), - _controller.HttpContext.RequestAborted); - } - - [Theory] - [InlineData(0, 5, null, null, false)] - [InlineData(5, 25, "2023-04-26T14:50:55.0596678-07:00", null, true)] - [InlineData(25, 1, null, "2023-04-26T14:50:55.0596678-07:00", false)] - [InlineData(int.MaxValue + 10L, 3, "2023-04-26T14:50:55.0596678-07:00", "2023-04-26T14:54:18.6773316-07:00", true)] - public async Task GivenParameters_WhenFetchingWindowedChangeFeed_ThenPassValues(long offset, int limit, string start, string end, bool includeMetadata) - { - DateTimeOffset? startTime = start != null ? DateTimeOffset.Parse(start, CultureInfo.InvariantCulture) : null; - DateTimeOffset? endTime = end != null ? DateTimeOffset.Parse(end, CultureInfo.InvariantCulture) : null; - var expectedRange = new TimeRange(startTime ?? DateTimeOffset.MinValue, endTime ?? DateTimeOffset.MaxValue); - var expected = new List(); - - _mediator - .Send( - Arg.Is(x => - x.Range == expectedRange && - x.Offset == offset && - x.Limit == limit && - x.Order == ChangeFeedOrder.Time && - x.IncludeMetadata == includeMetadata), - _controller.HttpContext.RequestAborted) - .Returns(new ChangeFeedResponse(expected)); - - var options = new WindowedPaginationOptions - { - EndTime = endTime, - Limit = limit, - Offset = offset, - StartTime = startTime, - }; - - IActionResult result = await _controller.GetChangeFeedAsync(options, includeMetadata); - var actual = result as ObjectResult; - Assert.Same(expected, actual.Value); - - await _mediator - .Received(1) - .Send( - Arg.Is(x => - x.Range == expectedRange && - x.Offset == offset && - x.Limit == limit && - x.Order == ChangeFeedOrder.Time && - x.IncludeMetadata == includeMetadata), - _controller.HttpContext.RequestAborted); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task GivenParameters_WhenFetchingLatestChangeFeed_ThenPassValues(bool includeMetadata) - { - var expected = new ChangeFeedEntry(1, DateTimeOffset.UtcNow, ChangeFeedAction.Create, "1", "2", "3", 1, 1, ChangeFeedState.Current); - - _mediator - .Send( - Arg.Is(x => x.IncludeMetadata == includeMetadata), - _controller.HttpContext.RequestAborted) - .Returns(new ChangeFeedLatestResponse(expected)); - - IActionResult result = await _controller.GetChangeFeedLatestAsync(includeMetadata); - var actual = result as ObjectResult; - Assert.Same(expected, actual.Value); - - await _mediator - .Received(1) - .Send( - Arg.Is(x => x.IncludeMetadata == includeMetadata), - _controller.HttpContext.RequestAborted); - } - - [Theory] - [InlineData("1.0-prerelease", ChangeFeedOrder.Sequence)] - [InlineData("1.0", ChangeFeedOrder.Sequence)] - [InlineData("2.0", ChangeFeedOrder.Time)] - [InlineData("8.9", ChangeFeedOrder.Time)] - public async Task GivenVersion_WhenFetchingLatestChangeFeed_ThenChangeSortOrder(string version, ChangeFeedOrder order) - { - var expected = new ChangeFeedEntry(1, DateTimeOffset.UtcNow, ChangeFeedAction.Create, "1", "2", "3", 1, 1, ChangeFeedState.Current); - - IApiVersioningFeature versioningFeature = Substitute.For(); - _controller.ControllerContext.HttpContext.Features.Set(versioningFeature); - versioningFeature.RequestedApiVersion.Returns(ApiVersion.Parse(version)); - _mediator - .Send( - Arg.Is(x => x.Order == order), - _controller.HttpContext.RequestAborted) - .Returns(new ChangeFeedLatestResponse(expected)); - - IActionResult result = await _controller.GetChangeFeedLatestAsync(); - var actual = result as ObjectResult; - Assert.Same(expected, actual.Value); - - await _mediator - .Received(1) - .Send( - Arg.Is(x => x.Order == order), - _controller.HttpContext.RequestAborted); - } -} diff --git a/src/Microsoft.Health.Dicom.Api.UnitTests/Controllers/ControllerMetadataTests.cs b/src/Microsoft.Health.Dicom.Api.UnitTests/Controllers/ControllerMetadataTests.cs deleted file mode 100644 index 6e3f6a119f..0000000000 --- a/src/Microsoft.Health.Dicom.Api.UnitTests/Controllers/ControllerMetadataTests.cs +++ /dev/null @@ -1,59 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Routing; -using Microsoft.Health.Api.Features.Audit; -using Microsoft.Health.Dicom.Api.Controllers; -using Microsoft.Health.Dicom.Api.Features.Conventions; -using Microsoft.Health.Dicom.Api.Modules; -using Xunit; - -namespace Microsoft.Health.Dicom.Api.UnitTests.Controllers; - -public class ControllerMetadataTests -{ - private readonly List _controllerTypes = typeof(WebModule).Assembly.ExportedTypes - .Where(t => t.IsSubclassOf(typeof(ControllerBase))) - .Where(t => !t.IsAbstract) - .ToList(); - - [Fact] - public void GivenApi_WhenCountingControllers_ThenFindExpectedNumber() - => Assert.Equal(10, _controllerTypes.Count); - - [Fact] - public void GivenExportControllers_WhenQueryingApiVersion_ThenSupportedfromV1() - { - var expectedStartedVersion = 1; - int? actualStartedVersion = Attribute.GetCustomAttributes(typeof(ExportController), typeof(IntroducedInApiVersionAttribute)) - .Select(a => ((IntroducedInApiVersionAttribute)a).Version) - .SingleOrDefault(); - Assert.Equal(expectedStartedVersion, actualStartedVersion); - } - - [Fact] - public void GivenControllerActions_WhenQueryingAttributes_ThenFindAuditEventType() - { - foreach (Type controllerType in _controllerTypes) - { - // By convention, all of our actions are annotated by an HttpMethodAttribute - List actions = controllerType - .GetMethods(BindingFlags.Public | BindingFlags.Instance) - .Where(t => Attribute.GetCustomAttribute(t, typeof(HttpMethodAttribute)) != null) - .ToList(); - - Assert.True(actions.Count > 0); - foreach (MethodInfo a in actions) - { - Assert.NotNull(Attribute.GetCustomAttribute(a, typeof(AuditEventTypeAttribute))); - } - } - } -} diff --git a/src/Microsoft.Health.Dicom.Api.UnitTests/Controllers/ExportControllerTests.cs b/src/Microsoft.Health.Dicom.Api.UnitTests/Controllers/ExportControllerTests.cs deleted file mode 100644 index 41ab5e9b9d..0000000000 --- a/src/Microsoft.Health.Dicom.Api.UnitTests/Controllers/ExportControllerTests.cs +++ /dev/null @@ -1,113 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Net; -using System.Threading.Tasks; -using MediatR; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using Microsoft.Extensions.Primitives; -using Microsoft.Health.Dicom.Api.Controllers; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Messages.Export; -using Microsoft.Health.Dicom.Core.Models.Export; -using Microsoft.Health.Operations; -using Microsoft.Net.Http.Headers; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Api.UnitTests.Controllers; - -public class ExportControllerTests -{ - [Fact] - public void GivenNullArguments_WhenConstructing_ThenThrowArgumentNullException() - { - var mediator = new Mediator(null); - - Assert.Throws(() => new ExportController( - null, - Options.Create(new FeatureConfiguration()), - NullLogger.Instance)); - - Assert.Throws(() => new ExportController( - mediator, - null, - NullLogger.Instance)); - - Assert.Throws(() => new ExportController( - mediator, - Options.Create(null), - NullLogger.Instance)); - - Assert.Throws(() => new ExportController( - mediator, - Options.Create(new FeatureConfiguration()), - null)); - } - - [Fact] - public async Task GivenExportDisabled_WhenCallingApi_ThenShouldReturnNotFound() - { - IMediator _mediator = Substitute.For(); - var controller = new ExportController( - _mediator, - Options.Create(new FeatureConfiguration { EnableExport = false }), - NullLogger.Instance); - var spec = new ExportSpecification - { - Source = new ExportDataOptions(ExportSourceType.Identifiers), - Destination = new ExportDataOptions(ExportDestinationType.AzureBlob), - }; - - IActionResult result = await controller.ExportAsync(spec); - Assert.IsType(result); - } - - [Fact] - public async Task GivenExportEnabled_WhenCallingApi_ThenShouldReturnResult() - { - IMediator mediator = Substitute.For(); - var controller = new ExportController( - mediator, - Options.Create(new FeatureConfiguration { EnableExport = true }), - NullLogger.Instance); - - controller.ControllerContext.HttpContext = new DefaultHttpContext(); - - var operationId = Guid.NewGuid(); - var expected = new OperationReference(operationId, new Uri($"http://dicom.unit.test/operations/{operationId}")); - var spec = new ExportSpecification - { - Source = new ExportDataOptions(ExportSourceType.Identifiers), - Destination = new ExportDataOptions(ExportDestinationType.AzureBlob), - }; - - mediator - .Send( - Arg.Is(x => ReferenceEquals(spec, x.Specification)), - controller.HttpContext.RequestAborted) - .Returns(new ExportResponse(expected)); - - IActionResult result = await controller.ExportAsync(spec); - Assert.IsType(result); - - var actual = result as ObjectResult; - Assert.Equal((int)HttpStatusCode.Accepted, actual.StatusCode); - Assert.True(controller.Response.Headers.TryGetValue(HeaderNames.Location, out StringValues header)); - Assert.Single(header); - Assert.Same(expected, actual.Value); - Assert.Equal(expected.Href.AbsoluteUri, header[0]); - - await mediator - .Received(1) - .Send( - Arg.Is(x => ReferenceEquals(spec, x.Specification)), - controller.HttpContext.RequestAborted); - } -} diff --git a/src/Microsoft.Health.Dicom.Api.UnitTests/Controllers/ExtendedQueryTagControllerTests.cs b/src/Microsoft.Health.Dicom.Api.UnitTests/Controllers/ExtendedQueryTagControllerTests.cs deleted file mode 100644 index 57de81e87a..0000000000 --- a/src/Microsoft.Health.Dicom.Api.UnitTests/Controllers/ExtendedQueryTagControllerTests.cs +++ /dev/null @@ -1,164 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Mime; -using System.Threading.Tasks; -using FellowOakDicom; -using MediatR; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using Microsoft.Extensions.Primitives; -using Microsoft.Health.Dicom.Api.Controllers; -using Microsoft.Health.Dicom.Api.Models; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Messages.ExtendedQueryTag; -using Microsoft.Health.Operations; -using Microsoft.Net.Http.Headers; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Api.UnitTests.Extensions; - -public class ExtendedQueryTagControllerTests -{ - private readonly IOptions _featureConfiguration; - - public ExtendedQueryTagControllerTests() - { - _featureConfiguration = Options.Create(new FeatureConfiguration { DisableOperations = false }); - } - - [Fact] - public void GivenNullArguments_WhenConstructing_ThenThrowArgumentNullException() - { - var mediator = new Mediator(null); - Assert.Throws(() => new ExtendedQueryTagController(null, NullLogger.Instance, _featureConfiguration)); - Assert.Throws(() => new ExtendedQueryTagController(mediator, null, _featureConfiguration)); - } - - [Fact] - public async Task GivenExistingReindex_WhenCallingApi_ThenShouldReturnConflict() - { - IMediator mediator = Substitute.For(); - const string path = "11330001"; - - var controller = new ExtendedQueryTagController(mediator, NullLogger.Instance, _featureConfiguration); - controller.ControllerContext.HttpContext = new DefaultHttpContext(); - - Guid id = Guid.NewGuid(); - var expected = new OperationReference(id, new Uri("https://dicom.contoso.io/unit/test/Operations/" + id, UriKind.Absolute)); - - mediator - .Send( - Arg.Is(x => x.ExtendedQueryTags.Single().Path == path), - controller.HttpContext.RequestAborted) - .Returns(Task.FromException(new ExistingOperationException(expected, "re-index"))); - - var actual = await controller.PostAsync(new AddExtendedQueryTagEntry[] { new AddExtendedQueryTagEntry { Path = path } }) as ContentResult; - await mediator.Received(1).Send( - Arg.Is(x => x.ExtendedQueryTags.Single().Path == path), - controller.HttpContext.RequestAborted); - - Assert.NotNull(actual); - Assert.True(controller.Response.Headers.TryGetValue(HeaderNames.Location, out StringValues header)); - Assert.Single(header); - - Assert.Equal((int)HttpStatusCode.Conflict, actual.StatusCode); - Assert.Equal(MediaTypeNames.Text.Plain, actual.ContentType); - Assert.Contains(expected.Id.ToString(OperationId.FormatSpecifier), actual.Content); - Assert.Equal(expected.Href.AbsoluteUri, header[0]); - } - - [Fact] - public async Task GivenTagPath_WhenCallingApi_ThenShouldReturnOk() - { - IMediator mediator = Substitute.For(); - const string path = "11330001"; - - var controller = new ExtendedQueryTagController(mediator, NullLogger.Instance, _featureConfiguration); - controller.ControllerContext.HttpContext = new DefaultHttpContext(); - - var expected = new GetExtendedQueryTagErrorsResponse( - new List - { - new ExtendedQueryTagError(DateTime.UtcNow, Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), "Error 1"), - new ExtendedQueryTagError(DateTime.UtcNow, Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), "Error 2"), - new ExtendedQueryTagError(DateTime.UtcNow, Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), "Error 3"), - }); - - mediator - .Send( - Arg.Is(x => x.Path == path), - Arg.Is(controller.HttpContext.RequestAborted)) - .Returns(expected); - - IActionResult response = await controller.GetTagErrorsAsync(path, new PaginationOptions()); - Assert.IsType(response); - - var actual = response as ObjectResult; - Assert.Equal((int)HttpStatusCode.OK, actual.StatusCode); - Assert.Same(expected.ExtendedQueryTagErrors, actual.Value); - - await mediator.Received(1).Send( - Arg.Is(x => x.Path == path), - Arg.Is(controller.HttpContext.RequestAborted)); - } - - [Fact] - public async Task GivenOperationId_WhenAddingTags_ReturnIdWithHeader() - { - Guid id = Guid.NewGuid(); - var expected = new AddExtendedQueryTagResponse( - new OperationReference(id, new Uri("https://dicom.contoso.io/unit/test/Operations/" + id, UriKind.Absolute))); - IMediator mediator = Substitute.For(); - var controller = new ExtendedQueryTagController(mediator, NullLogger.Instance, _featureConfiguration); - controller.ControllerContext.HttpContext = new DefaultHttpContext(); - - var input = new List - { - new AddExtendedQueryTagEntry - { - Level = QueryTagLevel.Instance, - Path = "00101001", - VR = DicomVRCode.PN, - }, - new AddExtendedQueryTagEntry - { - Level = QueryTagLevel.Instance, - Path = "11330001", - PrivateCreator = "Microsoft", - VR = DicomVRCode.SS, - } - }; - - mediator - .Send( - Arg.Is(x => ReferenceEquals(x.ExtendedQueryTags, input)), - Arg.Is(controller.HttpContext.RequestAborted)) - .Returns(expected); - - var actual = await controller.PostAsync(input) as ObjectResult; - Assert.NotNull(actual); - Assert.IsType(actual.Value); - Assert.True(controller.Response.Headers.TryGetValue(HeaderNames.Location, out StringValues header)); - Assert.Single(header); - - Assert.Equal((int)HttpStatusCode.Accepted, actual.StatusCode); - Assert.Same(expected.Operation, actual.Value); - Assert.Equal("https://dicom.contoso.io/unit/test/Operations/" + id, header[0]); - - await mediator.Received(1).Send( - Arg.Is(x => ReferenceEquals(x.ExtendedQueryTags, input)), - Arg.Is(controller.HttpContext.RequestAborted)); - } -} diff --git a/src/Microsoft.Health.Dicom.Api.UnitTests/Controllers/OperationsControllerTests.cs b/src/Microsoft.Health.Dicom.Api.UnitTests/Controllers/OperationsControllerTests.cs deleted file mode 100644 index ee8c6fe149..0000000000 --- a/src/Microsoft.Health.Dicom.Api.UnitTests/Controllers/OperationsControllerTests.cs +++ /dev/null @@ -1,218 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Net; -using System.Threading.Tasks; -using MediatR; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Versioning; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Primitives; -using Microsoft.Health.Dicom.Api.Controllers; -using Microsoft.Health.Dicom.Core.Features.Routing; -using Microsoft.Health.Dicom.Core.Messages.Operations; -using Microsoft.Health.Dicom.Core.Models.Operations; -using Microsoft.Health.Operations; -using Microsoft.Net.Http.Headers; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Api.UnitTests.Controllers; - -public class OperationsControllerTests -{ - private readonly IMediator _mediator = Substitute.For(); - private readonly IUrlResolver _urlResolver = Substitute.For(); - private readonly IApiVersioningFeature _apiVersion = Substitute.For(); - private readonly OperationsController _controller; - - public OperationsControllerTests() - { - _apiVersion.RequestedApiVersion.Returns(new ApiVersion(1, 0)); - - _controller = new OperationsController(_mediator, _urlResolver, NullLogger.Instance); - _controller.ControllerContext.HttpContext = new DefaultHttpContext(); - _controller.HttpContext.Features.Set(_apiVersion); - } - - [Fact] - public void GivenNullArguments_WhenConstructing_ThenThrowArgumentNullException() - { - var mediator = new Mediator(null); - - Assert.Throws(() => new OperationsController( - null, - Substitute.For(), - NullLogger.Instance)); - - Assert.Throws(() => new OperationsController( - mediator, - null, - NullLogger.Instance)); - - Assert.Throws(() => new OperationsController( - mediator, - Substitute.For(), - null)); - } - - [Fact] - public async Task GivenNullState_WhenGettingState_ThenReturnNotFound() - { - Guid id = Guid.NewGuid(); - - _mediator - .Send( - Arg.Is(x => x.OperationId == id), - Arg.Is(_controller.HttpContext.RequestAborted)) - .Returns((OperationStateResponse)null); - - Assert.IsType(await _controller.GetStateAsync(id)); - Assert.False(_controller.Response.Headers.ContainsKey(HeaderNames.Location)); - - await _mediator.Received(1).Send( - Arg.Is(x => x.OperationId == id), - Arg.Is(_controller.HttpContext.RequestAborted)); - _urlResolver.DidNotReceiveWithAnyArgs().ResolveOperationStatusUri(default); - } - - [Theory] - [InlineData(OperationStatus.NotStarted)] - [InlineData(OperationStatus.Running)] - public async Task GivenInProgressState_WhenGettingState_ThenReturnOk(OperationStatus inProgressStatus) - { - Guid id = Guid.NewGuid(); - string statusUrl = "https://dicom.contoso.io/unit/test/Operations/" + id; - - var expected = new OperationState - { - CreatedTime = DateTime.UtcNow.AddMinutes(-1), - LastUpdatedTime = DateTime.UtcNow, - OperationId = id, - PercentComplete = inProgressStatus == OperationStatus.NotStarted ? 0 : 37, - Resources = new Uri[] { new Uri("https://dicom.contoso.io/unit/test/extendedquerytags/00101010", UriKind.Absolute) }, - Status = inProgressStatus, - Type = DicomOperation.Reindex, - }; - - _mediator - .Send( - Arg.Is(x => x.OperationId == id), - Arg.Is(_controller.HttpContext.RequestAborted)) - .Returns(new OperationStateResponse(expected)); - _urlResolver.ResolveOperationStatusUri(id).Returns(new Uri(statusUrl, UriKind.Absolute)); - - IActionResult response = await _controller.GetStateAsync(id); - Assert.IsType(response); - Assert.True(_controller.Response.Headers.TryGetValue(HeaderNames.Location, out StringValues headerValue)); - Assert.Single(headerValue); - - var actual = response as ObjectResult; - Assert.Equal((int)HttpStatusCode.Accepted, actual.StatusCode); - Assert.Same(expected, actual.Value); - Assert.Equal(statusUrl, headerValue[0]); - - await _mediator.Received(1).Send( - Arg.Is(x => x.OperationId == id), - Arg.Is(_controller.HttpContext.RequestAborted)); - _urlResolver.Received(1).ResolveOperationStatusUri(id); - } - - [Theory] - [InlineData(OperationStatus.Unknown)] - [InlineData(OperationStatus.Failed)] - [InlineData(OperationStatus.Canceled)] - public async Task GivenDoneState_WhenGettingState_ThenReturnOk(OperationStatus doneStatus) - { - Guid id = Guid.NewGuid(); - - var expected = new OperationState - { - CreatedTime = DateTime.UtcNow.AddMinutes(-5), - LastUpdatedTime = DateTime.UtcNow, - OperationId = id, - PercentComplete = doneStatus == OperationStatus.Succeeded ? 100 : 71, - Resources = new Uri[] { new Uri("https://dicom.contoso.io/unit/test/extendedquerytags/00101010", UriKind.Absolute) }, - Status = doneStatus, - Type = DicomOperation.Reindex, - }; - - _mediator - .Send( - Arg.Is(x => x.OperationId == id), - Arg.Is(_controller.HttpContext.RequestAborted)) - .Returns(new OperationStateResponse(expected)); - - IActionResult response = await _controller.GetStateAsync(id); - Assert.IsType(response); - Assert.False(_controller.Response.Headers.ContainsKey(HeaderNames.Location)); - - var actual = response as ObjectResult; - Assert.Equal((int)HttpStatusCode.OK, actual.StatusCode); - Assert.Same(expected, actual.Value); - - await _mediator.Received(1).Send( - Arg.Is(x => x.OperationId == id), - Arg.Is(_controller.HttpContext.RequestAborted)); - _urlResolver.DidNotReceiveWithAnyArgs().ResolveOperationStatusUri(default); - } - - [Theory] -#pragma warning disable CS0618 // Type or member is obsolete - [InlineData(null, OperationStatus.Completed)] - [InlineData(1, OperationStatus.Completed)] -#pragma warning restore CS0618 // Type or member is obsolete - [InlineData(2, OperationStatus.Succeeded)] - [InlineData(3, OperationStatus.Succeeded)] - public async Task GivenSucceededState_WhenGettingState_ThenReturnProperDoneStatus(int? apiVersion, OperationStatus expectedStatus) - { - Guid id = Guid.NewGuid(); - DateTime utcNow = DateTime.UtcNow; - - _apiVersion.RequestedApiVersion.Returns(apiVersion.HasValue ? new ApiVersion(apiVersion.GetValueOrDefault(), 0) : null); - - var expected = new OperationState - { - CreatedTime = utcNow.AddMinutes(-5), - LastUpdatedTime = utcNow, - OperationId = id, - PercentComplete = 100, - Resources = new Uri[] { new Uri("https://dicom.contoso.io/unit/test/extendedquerytags/00101010", UriKind.Absolute) }, - Results = new object(), - Status = expectedStatus, - Type = DicomOperation.Reindex, - }; - - _mediator - .Send( - Arg.Is(x => x.OperationId == id), - Arg.Is(_controller.HttpContext.RequestAborted)) - .Returns(new OperationStateResponse(expected)); - - IActionResult response = await _controller.GetStateAsync(id); - Assert.IsType(response); - Assert.False(_controller.Response.Headers.ContainsKey(HeaderNames.Location)); - - var objectResult = response as ObjectResult; - Assert.Equal((int)HttpStatusCode.OK, objectResult.StatusCode); - - var actual = objectResult.Value as IOperationState; - Assert.Equal(expected.CreatedTime, actual.CreatedTime); - Assert.Equal(expected.LastUpdatedTime, actual.LastUpdatedTime); - Assert.Equal(expected.OperationId, actual.OperationId); - Assert.Equal(expected.PercentComplete, actual.PercentComplete); - Assert.Same(expected.Resources, actual.Resources); - Assert.Same(expected.Results, actual.Results); - Assert.Equal(expectedStatus, actual.Status); - Assert.Equal(expected.Type, actual.Type); - - await _mediator.Received(1).Send( - Arg.Is(x => x.OperationId == id), - Arg.Is(_controller.HttpContext.RequestAborted)); - _urlResolver.DidNotReceiveWithAnyArgs().ResolveOperationStatusUri(default); - } -} diff --git a/src/Microsoft.Health.Dicom.Api.UnitTests/Controllers/WorkitemController.AddTests.cs b/src/Microsoft.Health.Dicom.Api.UnitTests/Controllers/WorkitemController.AddTests.cs deleted file mode 100644 index 87455a0903..0000000000 --- a/src/Microsoft.Health.Dicom.Api.UnitTests/Controllers/WorkitemController.AddTests.cs +++ /dev/null @@ -1,103 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Net; -using System.Threading.Tasks; -using FellowOakDicom; -using MediatR; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Primitives; -using Microsoft.Health.Dicom.Api.Controllers; -using Microsoft.Health.Dicom.Core.Messages.Workitem; -using Microsoft.Net.Http.Headers; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Api.UnitTests.Controllers; - -public sealed class WorkitemControllerAddTests -{ - private readonly WorkitemController _controller; - private readonly IMediator _mediator; - private readonly string _id = Guid.NewGuid().ToString(); - - public WorkitemControllerAddTests() - { - _mediator = Substitute.For(); - _controller = new WorkitemController(_mediator, NullLogger.Instance); - _controller.ControllerContext.HttpContext = new DefaultHttpContext(); - _controller.ControllerContext.HttpContext.Request.Query = new QueryCollection(new Dictionary { { _id, StringValues.Empty } }); - - } - - [Fact] - public void GivenNullArguments_WhenConstructing_ThenThrowArgumentNullException() - { - var mediator = new Mediator(null); - Assert.Throws(() => new WorkitemController( - null, - NullLogger.Instance)); - - Assert.Throws(() => new WorkitemController( - mediator, - null)); - } - - [Fact] - public async Task GivenWorkitemInstanceUid_WhenHandlerFails_ThenReturnBadRequest() - { - _mediator - .Send( - Arg.Is(x => x.WorkitemInstanceUid == _id), - Arg.Is(_controller.HttpContext.RequestAborted)) - .Returns(new AddWorkitemResponse(WorkitemResponseStatus.Failure, new Uri("https://www.microsoft.com"))); - - ObjectResult result = await _controller.AddAsync(new DicomDataset[1]) as ObjectResult; - - Assert.IsType(result); - Assert.Equal(HttpStatusCode.BadRequest, (HttpStatusCode)result.StatusCode); - Assert.False(_controller.Response.Headers.ContainsKey(HeaderNames.ContentLocation)); - } - - [Fact] - public async Task GivenWorkitemInstanceUid_WhenItAlreadyExists_ThenReturnConflict() - { - _mediator - .Send( - Arg.Is(x => x.WorkitemInstanceUid == _id), - Arg.Is(_controller.HttpContext.RequestAborted)) - .Returns(new AddWorkitemResponse(WorkitemResponseStatus.Conflict, new Uri("https://www.microsoft.com"))); - - ObjectResult result = await _controller.AddAsync(new DicomDataset[1]) as ObjectResult; - - Assert.IsType(result); - Assert.Equal(HttpStatusCode.Conflict, (HttpStatusCode)result.StatusCode); - Assert.False(_controller.Response.Headers.ContainsKey(HeaderNames.ContentLocation)); - } - - - [Fact] - public async Task GivenWorkitemInstanceUid_WhenHandlerSucceeds_ThenReturnCreated() - { - var url = "https://www.microsoft.com/"; - _mediator - .Send( - Arg.Is(x => x.WorkitemInstanceUid == _id), - Arg.Is(_controller.HttpContext.RequestAborted)) - .Returns(new AddWorkitemResponse(WorkitemResponseStatus.Success, new Uri(url))); - - ObjectResult result = await _controller.AddAsync(new DicomDataset[1]) as ObjectResult; - - Assert.IsType(result); - Assert.Equal(HttpStatusCode.Created, (HttpStatusCode)result.StatusCode); - Assert.True(_controller.Response.Headers.ContainsKey(HeaderNames.ContentLocation)); - Assert.Equal(url, _controller.Response.Headers[HeaderNames.ContentLocation]); - Assert.Equal(url, _controller.Response.Headers[HeaderNames.Location]); - } -} diff --git a/src/Microsoft.Health.Dicom.Api.UnitTests/Controllers/WorkitemController.RetrieveTests.cs b/src/Microsoft.Health.Dicom.Api.UnitTests/Controllers/WorkitemController.RetrieveTests.cs deleted file mode 100644 index dc0d98012e..0000000000 --- a/src/Microsoft.Health.Dicom.Api.UnitTests/Controllers/WorkitemController.RetrieveTests.cs +++ /dev/null @@ -1,96 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Net; -using System.Threading.Tasks; -using FellowOakDicom; -using MediatR; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Primitives; -using Microsoft.Health.Dicom.Api.Controllers; -using Microsoft.Health.Dicom.Core.Messages.Workitem; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Api.UnitTests.Controllers; - -public sealed class WorkitemControllerRetrieveTests -{ - private readonly WorkitemController _controller; - private readonly IMediator _mediator; - private readonly string _id = Guid.NewGuid().ToString(); - - public WorkitemControllerRetrieveTests() - { - _mediator = Substitute.For(); - _controller = new WorkitemController(_mediator, NullLogger.Instance); - _controller.ControllerContext.HttpContext = new DefaultHttpContext(); - _controller.ControllerContext.HttpContext.Request.Query = new QueryCollection(new Dictionary { { _id, StringValues.Empty } }); - } - - [Fact] - public void GivenNullArguments_WhenConstructing_ThenThrowArgumentNullException() - { - var mediator = new Mediator(null); - - Assert.Throws(() => new WorkitemController( - null, - NullLogger.Instance)); - - Assert.Throws(() => new WorkitemController( - mediator, - null)); - } - - [Fact] - public async Task GivenWorkitemInstanceUid_WhenHandlerFails_ThenReturnBadRequest() - { - _mediator - .Send( - Arg.Is(x => x.WorkitemInstanceUid == _id), - Arg.Is(_controller.HttpContext.RequestAborted)) - .Returns(new RetrieveWorkitemResponse(WorkitemResponseStatus.Failure, new DicomDataset(), string.Empty)); - - var result = await _controller.RetrieveAsync(_id) as ObjectResult; - - Assert.IsType(result); - Assert.Equal(HttpStatusCode.BadRequest, (HttpStatusCode)result.StatusCode); - } - - [Fact] - public async Task GivenWorkitemInstanceUid_WhenHandlerSucceeds_ThenReturnOK() - { - _mediator - .Send( - Arg.Is(x => x.WorkitemInstanceUid == _id), - Arg.Is(_controller.HttpContext.RequestAborted)) - .Returns(new RetrieveWorkitemResponse(WorkitemResponseStatus.Success, new DicomDataset(), string.Empty)); - - var result = await _controller.RetrieveAsync(_id) as ObjectResult; - - Assert.IsType(result); - Assert.Equal(HttpStatusCode.OK, (HttpStatusCode)result.StatusCode); - } - - [Fact] - public async Task GivenWorkitemInstanceUid_WhenHandlerFails_ThenReturnNotFound() - { - _mediator - .Send( - Arg.Is(x => x.WorkitemInstanceUid == _id), - Arg.Is(_controller.HttpContext.RequestAborted)) - .Returns(new RetrieveWorkitemResponse(WorkitemResponseStatus.NotFound, new DicomDataset(), string.Empty)); - - var result = await _controller.RetrieveAsync(_id) as ObjectResult; - - Assert.IsType(result); - Assert.Equal(HttpStatusCode.NotFound, (HttpStatusCode)result.StatusCode); - } - -} diff --git a/src/Microsoft.Health.Dicom.Api.UnitTests/Controllers/WorkitemController.UpdateTests.cs b/src/Microsoft.Health.Dicom.Api.UnitTests/Controllers/WorkitemController.UpdateTests.cs deleted file mode 100644 index 3f8f57c8a9..0000000000 --- a/src/Microsoft.Health.Dicom.Api.UnitTests/Controllers/WorkitemController.UpdateTests.cs +++ /dev/null @@ -1,102 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Net; -using System.Threading.Tasks; -using FellowOakDicom; -using MediatR; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Primitives; -using Microsoft.Health.Dicom.Api.Controllers; -using Microsoft.Health.Dicom.Core.Messages.Workitem; -using Microsoft.Net.Http.Headers; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Api.UnitTests.Controllers; - -public sealed class WorkitemControllerUpdateTests -{ - private readonly WorkitemController _controller; - private readonly IMediator _mediator; - private readonly string _id = Guid.NewGuid().ToString(); - - public WorkitemControllerUpdateTests() - { - _mediator = Substitute.For(); - _controller = new WorkitemController(_mediator, NullLogger.Instance); - _controller.ControllerContext.HttpContext = new DefaultHttpContext(); - _controller.ControllerContext.HttpContext.Request.Query = new QueryCollection(new Dictionary { { _id, StringValues.Empty } }); - - } - - [Fact] - public void GivenNullArguments_WhenConstructing_ThenThrowArgumentNullException() - { - var mediator = new Mediator(null); - Assert.Throws(() => new WorkitemController( - null, - NullLogger.Instance)); - - Assert.Throws(() => new WorkitemController( - mediator, - null)); - } - - [Fact] - public async Task GivenWorkitemInstanceUid_WhenHandlerFails_ThenReturnBadRequest() - { - _mediator - .Send( - Arg.Is(x => x.WorkitemInstanceUid == _id), - Arg.Is(_controller.HttpContext.RequestAborted)) - .Returns(new UpdateWorkitemResponse(WorkitemResponseStatus.Failure, new Uri("https://www.microsoft.com"))); - - ObjectResult result = await _controller.UpdateAsync(_id, new DicomDataset[1]) as ObjectResult; - - Assert.IsType(result); - Assert.Equal(HttpStatusCode.BadRequest, (HttpStatusCode)result.StatusCode); - Assert.False(_controller.Response.Headers.ContainsKey(HeaderNames.ContentLocation)); - } - - [Fact] - public async Task GivenWorkitemInstanceUid_WhenItAlreadyExists_ThenReturnConflict() - { - _mediator - .Send( - Arg.Is(x => x.WorkitemInstanceUid == _id), - Arg.Is(_controller.HttpContext.RequestAborted)) - .Returns(new UpdateWorkitemResponse(WorkitemResponseStatus.Conflict, new Uri("https://www.microsoft.com"))); - - ObjectResult result = await _controller.UpdateAsync(_id, new DicomDataset[1]) as ObjectResult; - - Assert.IsType(result); - Assert.Equal(HttpStatusCode.Conflict, (HttpStatusCode)result.StatusCode); - Assert.False(_controller.Response.Headers.ContainsKey(HeaderNames.ContentLocation)); - } - - - [Fact] - public async Task GivenWorkitemInstanceUid_WhenHandlerSucceeds_ThenReturnCreated() - { - var url = "https://www.microsoft.com/"; - _mediator - .Send( - Arg.Is(x => x.WorkitemInstanceUid == _id), - Arg.Is(_controller.HttpContext.RequestAborted)) - .Returns(new UpdateWorkitemResponse(WorkitemResponseStatus.Success, new Uri(url))); - - ObjectResult result = await _controller.UpdateAsync(_id, new DicomDataset[1]) as ObjectResult; - - Assert.IsType(result); - Assert.Equal(HttpStatusCode.OK, (HttpStatusCode)result.StatusCode); - Assert.True(_controller.Response.Headers.ContainsKey(HeaderNames.ContentLocation)); - Assert.Equal(url, _controller.Response.Headers[HeaderNames.ContentLocation]); - } -} diff --git a/src/Microsoft.Health.Dicom.Api.UnitTests/Extensions/HttpContextExtensionsTests.cs b/src/Microsoft.Health.Dicom.Api.UnitTests/Extensions/HttpContextExtensionsTests.cs deleted file mode 100644 index 36da17c792..0000000000 --- a/src/Microsoft.Health.Dicom.Api.UnitTests/Extensions/HttpContextExtensionsTests.cs +++ /dev/null @@ -1,47 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Versioning; -using Microsoft.Health.Dicom.Api.Extensions; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Api.UnitTests.Extensions; - -public class HttpContextExtensionsTests -{ - private readonly HttpContext _context; - private readonly IApiVersioningFeature _apiVersioningFeature = Substitute.For(); - - public HttpContextExtensionsTests() - { - _context = new DefaultHttpContext(); - _context.Features.Set(_apiVersioningFeature); - } - - [Fact] - public void GivenNoVersioningFeature_WhenGettingMajorVersion_ThenReturnDefault() - => Assert.Equal(1, _context.GetMajorRequestedApiVersion()); - - [Fact] - public void GivenNoVersion_WhenGettingMajorVersion_ThenReturnDefault() - { - _apiVersioningFeature.RequestedApiVersion.Returns((ApiVersion)null); - Assert.Equal(1, _context.GetMajorRequestedApiVersion()); - } - - [Theory] - [InlineData("1.0-prerelease", 1)] - [InlineData("1.0", 1)] - [InlineData("2.3", 2)] - [InlineData("56.78", 56)] - public void GivenApiVersion_WhenGettingMajorVersion_ThenParseVersion(string version, int expected) - { - _apiVersioningFeature.RequestedApiVersion.Returns(ApiVersion.Parse(version)); - Assert.Equal(expected, _context.GetMajorRequestedApiVersion()); - } -} diff --git a/src/Microsoft.Health.Dicom.Api.UnitTests/Extensions/HttpRequestExtensionsTests.cs b/src/Microsoft.Health.Dicom.Api.UnitTests/Extensions/HttpRequestExtensionsTests.cs deleted file mode 100644 index a9400ff19b..0000000000 --- a/src/Microsoft.Health.Dicom.Api.UnitTests/Extensions/HttpRequestExtensionsTests.cs +++ /dev/null @@ -1,107 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using Microsoft.AspNetCore.Http; -using Microsoft.Health.Dicom.Api.Extensions; -using Microsoft.Health.Dicom.Core.Messages.Retrieve; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Api.UnitTests.Extensions; - -public class HttpRequestExtensionsTests -{ - [Fact] - public void GivenEmptyHeaders_WhenGetAcceptHeaders_ThenShouldReturnEmpty() - { - var httpRequest = Substitute.For(); - IHeaderDictionary headers = new HeaderDictionary(); - httpRequest.Headers.Returns(headers); - IEnumerable acceptHeaders = httpRequest.GetAcceptHeaders(); - Assert.Empty(acceptHeaders); - } - - [Fact] - public void GivenNonEmptyHeaders_WhenGetAcceptHeaders_ThenShouldReturnHeaders() - { - var httpRequest = Substitute.For(); - HeaderDictionary headers = new() { { "accept", "application/dicom" } }; - httpRequest.Headers.Returns(headers); - IEnumerable acceptHeaders = httpRequest.GetAcceptHeaders(); - Assert.Single(acceptHeaders); - } - - [Fact] - public void GivenNullHttpRequest_WhenGetAcceptHeaders_ThenShouldThrowException() - { - Assert.Throws(() => HttpRequestExtensions.GetAcceptHeaders(null)); - } - - [Fact] - public void GivenHttpRequest_WhenGetHostAndFollowDicomStandard_ThenShouldReturnExpectedValue() - { - string host = "host1"; - var httpRequest = Substitute.For(); - httpRequest.Host.Returns(new HostString(host)); - Assert.Equal(host + ":", httpRequest.GetHost(dicomStandards: true)); - } - - [Fact] - public void GivenHttpRequestHavingNoHost_WhenGetHost_ThenShouldReturnExpectedValue() - { - var httpRequest = Substitute.For(); - httpRequest.Host.Returns(new HostString(string.Empty)); - Assert.Equal(string.Empty, httpRequest.GetHost(dicomStandards: true)); - } - - [Fact] - public void GivenHttpRequest_WhenGetHostNotFollowingDicomStandard_ThenShouldReturnExpectedValue() - { - string host = "host1"; - var httpRequest = Substitute.For(); - httpRequest.Host.Returns(new HostString(host)); - Assert.Equal(host, httpRequest.GetHost(dicomStandards: false)); - } - - [Fact] - public void GivenHttpRequestWithOriginalHeader_WhenGetIsOriginalVersionRequested_ThenShouldReturnExpectedValue() - { - HttpRequest httpRequest = Substitute.For(); - IHeaderDictionary headers = new HeaderDictionary - { - { "accept", "application/dicom" }, - { "msdicom-request-original", bool.TrueString } - }; - httpRequest.Headers.Returns(headers); - Assert.True(httpRequest.IsOriginalVersionRequested()); - } - - [Fact] - public void GivenHttpRequestWithNoOriginalHeader_WhenGetIsOriginalVersionRequested_ThenShouldReturnExpectedValue() - { - HttpRequest httpRequest = Substitute.For(); - IHeaderDictionary headers = new HeaderDictionary - { - { "accept", "application/dicom" } - }; - httpRequest.Headers.Returns(headers); - Assert.False(httpRequest.IsOriginalVersionRequested()); - } - - [Fact] - public void GivenHttpRequestWithEmptyOriginalHeader_WhenGetIsOriginalVersionRequested_ThenShouldReturnExpectedValue() - { - HttpRequest httpRequest = Substitute.For(); - IHeaderDictionary headers = new HeaderDictionary - { - { "accept", "application/dicom" }, - { "msdicom-request-original", string.Empty } - }; - httpRequest.Headers.Returns(headers); - Assert.False(httpRequest.IsOriginalVersionRequested()); - } -} diff --git a/src/Microsoft.Health.Dicom.Api.UnitTests/Extensions/HttpResponseExtensionsTests.cs b/src/Microsoft.Health.Dicom.Api.UnitTests/Extensions/HttpResponseExtensionsTests.cs deleted file mode 100644 index a9712fcf63..0000000000 --- a/src/Microsoft.Health.Dicom.Api.UnitTests/Extensions/HttpResponseExtensionsTests.cs +++ /dev/null @@ -1,99 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Net.Http.Headers; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Primitives; -using Microsoft.Health.Dicom.Api.Extensions; -using Microsoft.Net.Http.Headers; -using Xunit; - -namespace Microsoft.Health.Dicom.Api.UnitTests.Extensions; - -public class HttpResponseExtensionsTests -{ - [Fact] - public void GivenNullParameters_WhenAddLocationHeader_ThenThrowsArgumentNullException() - { - var context = new DefaultHttpContext(); - var uri = new Uri("https://example.host.com/unit/tests?method=AddLocationHeader#GivenNullArguments_ThrowException"); - - Assert.Throws(() => HttpResponseExtensions.AddLocationHeader(null, uri)); - Assert.Throws(() => HttpResponseExtensions.AddLocationHeader(context.Response, null)); - } - - [Theory] - [InlineData("https://absolute.url:8080/there are spaces", "https://absolute.url:8080/there%20are%20spaces")] - [InlineData("/relative/url?with=query&string=and#fragment", "/relative/url?with=query&string=and#fragment")] - public void GivenValidLocationHeader_WhenAddLocationHeader_ThenShouldAddExpectedHeader(string actual, string expected) - { - var response = new DefaultHttpContext().Response; - var uri = new Uri(actual, UriKind.RelativeOrAbsolute); - - Assert.False(response.Headers.ContainsKey(HeaderNames.Location)); - response.AddLocationHeader(uri); - - Assert.True(response.Headers.TryGetValue(HeaderNames.Location, out StringValues headerValue)); - Assert.Single(headerValue); - Assert.Equal(expected, headerValue[0]); // Should continue to be escaped! - } - - [Fact] - public void GivenNullParameters_WhenTryAddErroneousAttributesHeader_ThenThrowsArgumentNullException() - { - var context = new DefaultHttpContext(); - Assert.Throws(() => HttpResponseExtensions.TryAddErroneousAttributesHeader(null, Array.Empty())); - Assert.Throws(() => HttpResponseExtensions.TryAddErroneousAttributesHeader(context.Response, null)); - } - - [Fact] - public void GivenEmptyErroneousTags_WhenTryAddErroneousAttributesHeader_ThenShouldReturnFalse() - { - var context = new DefaultHttpContext(); - Assert.False(HttpResponseExtensions.TryAddErroneousAttributesHeader(context.Response, Array.Empty())); - } - - [Fact] - public void GivenNonEmptyErroneousTags_WhenTryAddErroneousAttributesHeader_ThenShouldAddExpectedHeader() - { - var context = new DefaultHttpContext(); - var tags = new string[] { "PatientAge", "00011231" }; - Assert.True(HttpResponseExtensions.TryAddErroneousAttributesHeader(context.Response, tags)); - - Assert.True(context.Response.Headers.TryGetValue(HttpResponseExtensions.ErroneousAttributesHeader, out StringValues headerValue)); - Assert.Single(headerValue); - Assert.Equal(string.Join(",", tags), headerValue[0]); // Should continue to be escaped! - } - - [Fact] - public void GivenValidInput_WhenSetWarningHeader_ThenShouldHaveExpectedValue() - { - var context = new DefaultHttpContext(); - var warningCode = Core.Models.HttpWarningCode.MiscPersistentWarning; - string host = "host"; - string message = "message"; - context.Response.SetWarning(warningCode, host, message); - var warning = WarningHeaderValue.Parse(context.Response.Headers.Warning); - Assert.Equal((int)warningCode, warning.Code); - Assert.Equal(host, warning.Agent); - Assert.Equal("\"" + message + "\"", warning.Text); - - } - - [Fact] - public void GivenEmptyHost_WhenSetWarningHeader_ThenShouldHaveExpectedValue() - { - var context = new DefaultHttpContext(); - var warningCode = Core.Models.HttpWarningCode.MiscPersistentWarning; - string host = string.Empty; - string message = "message"; - context.Response.SetWarning(warningCode, host, message); - var warning = WarningHeaderValue.Parse(context.Response.Headers.Warning); - Assert.Equal((int)warningCode, warning.Code); - Assert.Equal("-", warning.Agent); - Assert.Equal("\"" + message + "\"", warning.Text); - } -} diff --git a/src/Microsoft.Health.Dicom.Api.UnitTests/Extensions/MediaTypeHeaderValueExtensionsTests.cs b/src/Microsoft.Health.Dicom.Api.UnitTests/Extensions/MediaTypeHeaderValueExtensionsTests.cs deleted file mode 100644 index c1d87c5ef1..0000000000 --- a/src/Microsoft.Health.Dicom.Api.UnitTests/Extensions/MediaTypeHeaderValueExtensionsTests.cs +++ /dev/null @@ -1,160 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using Microsoft.Extensions.Primitives; -using Microsoft.Health.Dicom.Api.Extensions; -using Microsoft.Health.Dicom.Core; -using Microsoft.Health.Dicom.Core.Features.Retrieve; -using Microsoft.Health.Dicom.Core.Messages.Retrieve; -using Microsoft.Health.Dicom.Core.Web; -using Microsoft.Net.Http.Headers; -using Xunit; - -namespace Microsoft.Health.Dicom.Api.UnitTests.Extensions; - -public class MediaTypeHeaderValueExtensionsTests -{ - [Fact] - public void GivenValidHeader_WhenGetParameter_ShouldReturnCorrectValue() - { - string parameter = AcceptHeaderParameterNames.Type; - string parameterValue = KnownContentTypes.ImageJpeg; - MediaTypeHeaderValue headerValue = new MediaTypeHeaderValue(KnownContentTypes.MultipartRelated); - headerValue.Parameters.Add(CreateNameValueHeaderValue(parameter, parameterValue)); - Assert.Equal(parameterValue, headerValue.GetParameter(parameter)); - } - - [Fact] - public void GivenMultipleParameters_WhenGetParameter_ShouldReturnFirstValue() - { - string parameter = AcceptHeaderParameterNames.Type; - string parameterValue1 = KnownContentTypes.ImageJpeg, parameterValue2 = KnownContentTypes.ApplicationOctetStream; - MediaTypeHeaderValue headerValue = new MediaTypeHeaderValue(KnownContentTypes.MultipartRelated); - headerValue.Parameters.Add(CreateNameValueHeaderValue(parameter, parameterValue1)); - headerValue.Parameters.Add(CreateNameValueHeaderValue(parameter, parameterValue2)); - Assert.Equal(parameterValue1, headerValue.GetParameter(parameter)); - } - - [Fact] - public void GivenNoRequiredParameter_WhenGetParameter_ShouldReturnEmpty() - { - MediaTypeHeaderValue headerValue = new MediaTypeHeaderValue(KnownContentTypes.MultipartRelated); - Assert.Equal(StringSegment.Empty, headerValue.GetParameter(AcceptHeaderParameterNames.Type)); - } - - [Fact] - public void GivenParameterValueWithQuote_WhenGetParameterWithRemoveQuote_ShouldReturnValueWithoutQuote() - { - MediaTypeHeaderValue headerValue = new MediaTypeHeaderValue(KnownContentTypes.MultipartRelated); - string parameter = AcceptHeaderParameterNames.Type; - string parameterValue = KnownContentTypes.ApplicationOctetStream; - headerValue.Parameters.Add(CreateNameValueHeaderValue(parameter, parameterValue)); - Assert.Equal(parameterValue, headerValue.GetParameter(parameter)); - } - - [Fact] - public void GivenParameterValueWithQuote_WhenGetParameterWithoutRemoveQuote_ShouldReturnValueWithQuote() - { - MediaTypeHeaderValue headerValue = new MediaTypeHeaderValue(KnownContentTypes.MultipartRelated); - string parameter = AcceptHeaderParameterNames.Type; - string parameterValue = KnownContentTypes.ApplicationOctetStream; - string parameterValueWithQuote = QuoteText(parameterValue); - headerValue.Parameters.Add(CreateNameValueHeaderValue(parameter, parameterValue)); - Assert.Equal(parameterValueWithQuote, headerValue.GetParameter(parameter, tryRemoveQuotes: false)); - } - - [Fact] - public void GivenSinglePartHeader_WhenGetAcceptHeader_ShouldSucceed() - { - string mediaType = KnownContentTypes.ApplicationDicom; - string transferSyntax = DicomTransferSyntaxUids.Original; - double quality = 0.9; - MediaTypeHeaderValue headerValue = CreateMediaTypeHeaderValue(mediaType, string.Empty, transferSyntax, quality); - AcceptHeader acceptHeader = headerValue.ToAcceptHeader(); - Assert.Equal(PayloadTypes.SinglePart, acceptHeader.PayloadType); - Assert.Equal(mediaType, acceptHeader.MediaType); - Assert.Equal(transferSyntax, acceptHeader.TransferSyntax); - Assert.Equal(quality, acceptHeader.Quality); - } - - [Fact] - public void GivenMultiPartRelatedHeader_WhenGetAcceptHeader_ShouldSucceed() - { - string type = KnownContentTypes.ApplicationOctetStream; - string transferSyntax = DicomTransferSyntaxUids.Original; - - double quality = 0.9; - MediaTypeHeaderValue headerValue = CreateMediaTypeHeaderValue(KnownContentTypes.MultipartRelated, type, transferSyntax, quality); - AcceptHeader acceptHeader = headerValue.ToAcceptHeader(); - Assert.Equal(PayloadTypes.MultipartRelated, acceptHeader.PayloadType); - Assert.Equal(type, acceptHeader.MediaType); - Assert.Equal(transferSyntax, acceptHeader.TransferSyntax); - Assert.Equal(quality, acceptHeader.Quality); - } - - /// - /// used by OHIF viewer, we will continue to recommend quotes in our conformance doc - /// - [Fact] - public void GivenMultiPartRelatedHeaderWithNoQuotesOnType_WhenGetAcceptHeader_ShouldSucceed() - { - var newMediaTypes = MediaTypeHeaderValue.ParseList(new List { "multipart/related; type=application/octet-stream; transfer-syntax=*" }); - MediaTypeHeaderValue headerValue = newMediaTypes.First(); - - AcceptHeader acceptHeader = headerValue.ToAcceptHeader(); - Assert.Equal(PayloadTypes.MultipartRelated, acceptHeader.PayloadType); - Assert.Equal("application/octet-stream", acceptHeader.MediaType); - Assert.Equal("*", acceptHeader.TransferSyntax); - } - - [Fact] - public void GivenMultiPartButNotRelatedHeader_WhenGetAcceptHeader_ShouldSucceed() - { - string mediaType = "multipart/form-data"; - string type = KnownContentTypes.ApplicationDicom; - string transferSyntax = DicomTransferSyntaxUids.Original; - double quality = 0.9; - MediaTypeHeaderValue headerValue = CreateMediaTypeHeaderValue(mediaType, type, transferSyntax, quality); - AcceptHeader acceptHeader = headerValue.ToAcceptHeader(); - Assert.Equal(PayloadTypes.SinglePart, acceptHeader.PayloadType); - Assert.Equal(mediaType, acceptHeader.MediaType); - Assert.Equal(transferSyntax, acceptHeader.TransferSyntax); - Assert.Equal(quality, acceptHeader.Quality); - } - - private static MediaTypeHeaderValue CreateMediaTypeHeaderValue(string mediaType, string type, string transferSyntax, double? quality) - { - MediaTypeHeaderValue result = new MediaTypeHeaderValue(mediaType); - if (!string.IsNullOrEmpty(type)) - { - result.Parameters.Add(CreateNameValueHeaderValue(AcceptHeaderParameterNames.Type, type, quoteValue: true)); - } - - if (!string.IsNullOrEmpty(transferSyntax)) - { - result.Parameters.Add(CreateNameValueHeaderValue(AcceptHeaderParameterNames.TransferSyntax, transferSyntax, quoteValue: false)); - } - - if (quality.HasValue) - { - result.Parameters.Add(CreateNameValueHeaderValue(AcceptHeaderParameterNames.Quality, quality.GetValueOrDefault().ToString(CultureInfo.InvariantCulture), quoteValue: false)); - } - - return result; - } - - private static string QuoteText(string text) - { - return $"\"{text}\""; - } - - private static NameValueHeaderValue CreateNameValueHeaderValue(string key, string value, bool quoteValue = true) - { - return new NameValueHeaderValue(key, quoteValue ? QuoteText(value) : value); - } -} diff --git a/src/Microsoft.Health.Dicom.Api.UnitTests/Extensions/QueryOptionsExtensionsTests.cs b/src/Microsoft.Health.Dicom.Api.UnitTests/Extensions/QueryOptionsExtensionsTests.cs deleted file mode 100644 index c0c387db55..0000000000 --- a/src/Microsoft.Health.Dicom.Api.UnitTests/Extensions/QueryOptionsExtensionsTests.cs +++ /dev/null @@ -1,82 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using Microsoft.Extensions.Primitives; -using Microsoft.Health.Dicom.Api.Extensions; -using Microsoft.Health.Dicom.Api.Models; -using Microsoft.Health.Dicom.Core.Features.Query; -using Xunit; - -namespace Microsoft.Health.Dicom.Api.UnitTests.Extensions; - -public class QueryOptionsExtensionsTests -{ - [Fact] - public void GivenDuplicateValues_WhenCreateParameters_ThrowQueryParseException() - { - var qsp = new List> - { - KeyValuePair.Create("PatientName", new StringValues(new string[] { "Foo", "Bar", "Baz" })), - }; - - Assert.Throws(() => new QueryOptions().ToQueryParameters(qsp, QueryResource.AllStudies)); - } - - [Fact] - public void GivenQueryString_WhenCreateParameters_IgnoreKnownParameters() - { - var qsp = new List> - { - KeyValuePair.Create("PatientName", new StringValues("Joe")), - KeyValuePair.Create("offset", new StringValues("10")), - KeyValuePair.Create("PatientBirthDate", new StringValues("18000101-19010101")), - KeyValuePair.Create("limit", new StringValues("50")), - KeyValuePair.Create("fuzzymatching", new StringValues("true")), - KeyValuePair.Create("ReferringPhysicianName", new StringValues("dr")), - KeyValuePair.Create("IncludeField", new StringValues("ManufacturerModelName")), - }; - - IReadOnlyDictionary actual = new QueryOptions().ToQueryParameters(qsp, QueryResource.AllStudies).Filters; - - Assert.Equal(3, actual.Count); - Assert.Equal("Joe", actual["PatientName"]); - Assert.Equal("18000101-19010101", actual["PatientBirthDate"]); - Assert.Equal("dr", actual["ReferringPhysicianName"]); - } - - [Fact] - public void GivenInput_WhenCreatingParameters_ThenAssignAppropriateValues() - { - var options = new QueryOptions - { - FuzzyMatching = true, - IncludeField = new List { "Modality" }, - Limit = 25, - Offset = 100, - }; - - string study = "foo"; - string series = "bar"; - - var qsp = new List> - { - KeyValuePair.Create(" PatientName ", new StringValues(" Will\t")), - KeyValuePair.Create("ReferringPhysicianName\r\n", new StringValues("dr")), - }; - - QueryParameters actual = options.ToQueryParameters(qsp, QueryResource.StudySeriesInstances, study, series); - Assert.Equal(2, actual.Filters.Count); - Assert.Equal("Will", actual.Filters["PatientName"]); - Assert.Equal("dr", actual.Filters["ReferringPhysicianName"]); - Assert.Equal(options.FuzzyMatching, actual.FuzzyMatching); - Assert.Same(options.IncludeField, actual.IncludeField); - Assert.Equal(options.Limit, actual.Limit); - Assert.Equal(options.Offset, actual.Offset); - Assert.Equal(QueryResource.StudySeriesInstances, actual.QueryResourceType); - Assert.Equal(series, actual.SeriesInstanceUid); - Assert.Equal(study, actual.StudyInstanceUid); - } -} diff --git a/src/Microsoft.Health.Dicom.Api.UnitTests/Extensions/StoreResponseStatusExtensionsTests.cs b/src/Microsoft.Health.Dicom.Api.UnitTests/Extensions/StoreResponseStatusExtensionsTests.cs deleted file mode 100644 index 11f9410fc7..0000000000 --- a/src/Microsoft.Health.Dicom.Api.UnitTests/Extensions/StoreResponseStatusExtensionsTests.cs +++ /dev/null @@ -1,41 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using Microsoft.Health.Dicom.Api.Extensions; -using Microsoft.Health.Dicom.Core.Messages.Store; -using Xunit; - -namespace Microsoft.Health.Dicom.Api.UnitTests.Extensions; - -public class StoreResponseStatusExtensionsTests -{ - public static IEnumerable GetStatusMapping() - { - yield return new object[] { StoreResponseStatus.None, HttpStatusCode.NoContent }; - yield return new object[] { StoreResponseStatus.Success, HttpStatusCode.OK }; - yield return new object[] { StoreResponseStatus.PartialSuccess, HttpStatusCode.Accepted }; - yield return new object[] { StoreResponseStatus.Failure, HttpStatusCode.Conflict }; - } - - [Theory] - [MemberData(nameof(GetStatusMapping))] - public void GivenStatus_WhenConvertedToHttpStatusCode_ThenCorrectStatusCodeShouldBeReturned(StoreResponseStatus status, HttpStatusCode expectedStatusCode) - { - Assert.Equal(expectedStatusCode, status.ToHttpStatusCode()); - } - - [Fact] - public void GivenStatus_WhenConvertingToHttpStatusCode_ThenMappingShouldExistForEachStatus() - { - // This test makes sure all of the mapping exists to prevent new status being added without new mapping. - Assert.Equal( - GetStatusMapping().Select(mapping => (StoreResponseStatus)mapping[0]), - Enum.GetValues(typeof(StoreResponseStatus)).Cast()); - } -} diff --git a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Audit/AuditHelperTests.cs b/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Audit/AuditHelperTests.cs deleted file mode 100644 index 9db1912a33..0000000000 --- a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Audit/AuditHelperTests.cs +++ /dev/null @@ -1,124 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Net; -using Microsoft.AspNetCore.Http; -using Microsoft.Health.Api.Features.Audit; -using Microsoft.Health.Core.Features.Audit; -using Microsoft.Health.Core.Features.Security; -using Microsoft.Health.Dicom.Api.Features.Audit; -using Microsoft.Health.Dicom.Core.Features.Audit; -using Microsoft.Health.Dicom.Core.Features.Context; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Api.UnitTests.Features.Audit; - -public class AuditHelperTests -{ - private const string AuditEventType = "audit"; - private const string CorrelationId = "correlation"; - private static readonly Uri Uri = new Uri("http://localhost/123"); - private static readonly IReadOnlyCollection> Claims = new List>(); - private static readonly IPAddress CallerIpAddress = new IPAddress(new byte[] { 0xA, 0x0, 0x0, 0x0 }); // 10.0.0.0 - private const string CallerIpAddressInString = "10.0.0.0"; - - private readonly IDicomRequestContextAccessor _dicomRequestContextAccessor = Substitute.For(); - private readonly IAuditLogger _auditLogger = Substitute.For(); - private readonly IAuditHeaderReader _auditHeaderReader = Substitute.For(); - - private readonly IDicomRequestContext _dicomRequestContext = Substitute.For(); - - private readonly IAuditHelper _auditHelper; - - private readonly HttpContext _httpContext = new DefaultHttpContext(); - private readonly IClaimsExtractor _claimsExtractor = Substitute.For(); - - public AuditHelperTests() - { - _dicomRequestContext.Uri.Returns(Uri); - _dicomRequestContext.CorrelationId.Returns(CorrelationId); - - _dicomRequestContextAccessor.RequestContext = _dicomRequestContext; - - _httpContext.Connection.RemoteIpAddress = CallerIpAddress; - - _claimsExtractor.Extract().Returns(Claims); - - _auditHelper = new AuditHelper(_dicomRequestContextAccessor, _auditLogger, _auditHeaderReader); - } - - [Fact] - public void GivenNoAuditEventType_WhenLogExecutingIsCalled_ThenAuditLogShouldNotBeLogged() - { - _auditHelper.LogExecuting(_httpContext, _claimsExtractor); - - _auditLogger.DidNotReceiveWithAnyArgs().LogAudit( - auditAction: default, - operation: default, - requestUri: default, - statusCode: default, - correlationId: default, - callerIpAddress: default, - callerClaims: default); - } - - [Fact] - public void GivenAuditEventType_WhenLogExecutingIsCalled_ThenAuditLogShouldBeLogged() - { - _dicomRequestContext.AuditEventType.Returns(AuditEventType); - - _auditHelper.LogExecuting(_httpContext, _claimsExtractor); - - _auditLogger.Received(1).LogAudit( - AuditAction.Executing, - AuditEventType, - requestUri: Uri, - statusCode: null, - correlationId: CorrelationId, - callerIpAddress: CallerIpAddressInString, - callerClaims: Claims, - customHeaders: _auditHeaderReader.Read(_httpContext)); - } - - [Fact] - public void GivenNoAuditEventType_WhenLogExecutedIsCalled_ThenAuditLogShouldNotBeLogged() - { - _auditHelper.LogExecuted(_httpContext, _claimsExtractor); - - _auditLogger.DidNotReceiveWithAnyArgs().LogAudit( - auditAction: default, - operation: default, - requestUri: default, - statusCode: default, - correlationId: default, - callerIpAddress: default, - callerClaims: default); - } - - [Fact] - public void GivenAuditEventType_WhenLogExecutedIsCalled_ThenAuditLogShouldBeLogged() - { - const HttpStatusCode expectedStatusCode = HttpStatusCode.Created; - - _dicomRequestContext.AuditEventType.Returns(AuditEventType); - - _httpContext.Response.StatusCode = (int)expectedStatusCode; - - _auditHelper.LogExecuted(_httpContext, _claimsExtractor); - - _auditLogger.Received(1).LogAudit( - AuditAction.Executed, - AuditEventType, - Uri, - expectedStatusCode, - CorrelationId, - CallerIpAddressInString, - Claims, - customHeaders: _auditHeaderReader.Read(_httpContext)); - } -} diff --git a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Audit/AuditLoggingFilterAttributeTests.cs b/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Audit/AuditLoggingFilterAttributeTests.cs deleted file mode 100644 index 244355a330..0000000000 --- a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Audit/AuditLoggingFilterAttributeTests.cs +++ /dev/null @@ -1,290 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Controllers; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.Routing; -using Microsoft.Health.Api.Features.Audit; -using Microsoft.Health.Core.Features.Security; -using Microsoft.Health.Dicom.Api.UnitTests.Features.Filters; -using NSubstitute; -using Xunit; -using DicomAudit = Microsoft.Health.Dicom.Api.Features.Audit; - -namespace Microsoft.Health.Dicom.Api.UnitTests.Features.Audit; - -public class AuditLoggingFilterAttributeTests -{ - private readonly IClaimsExtractor _claimsExtractor = Substitute.For(); - private readonly IAuditHelper _auditHelper = Substitute.For(); - - private readonly DicomAudit.AuditLoggingFilterAttribute _filter; - - private readonly HttpContext _httpContext = new DefaultHttpContext(); - - public AuditLoggingFilterAttributeTests() - { - _filter = new DicomAudit.AuditLoggingFilterAttribute(_claimsExtractor, _auditHelper); - } - - [Fact] - public void GivenChangeFeedController_WhenExecutingAction_ThenAuditLogShouldBeLogged() - { - var actionExecutingContext = new ActionExecutingContext( - new ActionContext(_httpContext, new RouteData(), new ControllerActionDescriptor() { DisplayName = "Executing ChangeFeed." }), - new List(), - new Dictionary(), - FilterTestsHelper.CreateMockChangeFeedController()); - - _filter.OnActionExecuting(actionExecutingContext); - - _auditHelper.Received(1).LogExecuting(_httpContext, _claimsExtractor); - } - - [Fact] - public void GivenChangeFeedController_WhenExecutedActionThrowsException_ThenAuditLogShouldNotBeLogged() - { - var result = new NoContentResult(); - - var actionExecutedContext = new ActionExecutedContext( - new ActionContext(_httpContext, new RouteData(), new ControllerActionDescriptor() { DisplayName = "Executed ChangeFeed." }), - new List(), - FilterTestsHelper.CreateMockChangeFeedController()); - - actionExecutedContext.Exception = new Exception("Test Exception."); - - _filter.OnActionExecuted(actionExecutedContext); - - _auditHelper.DidNotReceiveWithAnyArgs().LogExecuted( - httpContext: default, - claimsExtractor: default); - } - - [Fact] - public void GivenChangeFeedController_WhenExecutedAction_ThenAuditLogShouldNotBeLogged() - { - var result = new NoContentResult(); - - var resultExecutedContext = new ResultExecutedContext( - new ActionContext(_httpContext, new RouteData(), new ControllerActionDescriptor() { DisplayName = "Executed ChangeFeed." }), - new List(), - result, - FilterTestsHelper.CreateMockChangeFeedController()); - - _filter.OnResultExecuted(resultExecutedContext); - - _auditHelper.DidNotReceiveWithAnyArgs().LogExecuted( - httpContext: default, - claimsExtractor: default); - } - - [Fact] - public void GivenDeleteController_WhenExecutingAction_ThenAuditLogShouldBeLogged() - { - var actionExecutingContext = new ActionExecutingContext( - new ActionContext(_httpContext, new RouteData(), new ControllerActionDescriptor() { DisplayName = "Executing Delete." }), - new List(), - new Dictionary(), - FilterTestsHelper.CreateMockDeleteController()); - - _filter.OnActionExecuting(actionExecutingContext); - - _auditHelper.Received(1).LogExecuting(_httpContext, _claimsExtractor); - } - - [Fact] - public void GivenDeleteController_WhenExecutedActionThrowsException_ThenAuditLogShouldNotBeLogged() - { - var result = new NoContentResult(); - - var actionExecutedContext = new ActionExecutedContext( - new ActionContext(_httpContext, new RouteData(), new ControllerActionDescriptor() { DisplayName = "Executed Delete." }), - new List(), - FilterTestsHelper.CreateMockDeleteController()); - - actionExecutedContext.Exception = new Exception("Test Exception."); - - _filter.OnActionExecuted(actionExecutedContext); - - _auditHelper.DidNotReceiveWithAnyArgs().LogExecuted( - httpContext: default, - claimsExtractor: default); - } - - [Fact] - public void GivenDeleteController_WhenExecutedAction_ThenAuditLogShouldNotBeLogged() - { - var result = new NoContentResult(); - - var resultExecutedContext = new ResultExecutedContext( - new ActionContext(_httpContext, new RouteData(), new ControllerActionDescriptor() { DisplayName = "Executed Delete." }), - new List(), - result, - FilterTestsHelper.CreateMockDeleteController()); - - _filter.OnResultExecuted(resultExecutedContext); - - _auditHelper.DidNotReceiveWithAnyArgs().LogExecuted( - httpContext: default, - claimsExtractor: default); - } - - [Fact] - public void GivenQueryController_WhenExecutingAction_ThenAuditLogShouldBeLogged() - { - var actionExecutingContext = new ActionExecutingContext( - new ActionContext(_httpContext, new RouteData(), new ControllerActionDescriptor() { DisplayName = "Executing Query." }), - new List(), - new Dictionary(), - FilterTestsHelper.CreateMockQueryController()); - - _filter.OnActionExecuting(actionExecutingContext); - - _auditHelper.Received(1).LogExecuting(_httpContext, _claimsExtractor); - } - - [Fact] - public void GivenQueryController_WhenExecutedActionThrowsException_ThenAuditLogShouldNotBeLogged() - { - var result = new NoContentResult(); - - var actionExecutedContext = new ActionExecutedContext( - new ActionContext(_httpContext, new RouteData(), new ControllerActionDescriptor() { DisplayName = "Executed Query." }), - new List(), - FilterTestsHelper.CreateMockQueryController()); - - actionExecutedContext.Exception = new Exception("Test Exception."); - - _filter.OnActionExecuted(actionExecutedContext); - - _auditHelper.DidNotReceiveWithAnyArgs().LogExecuted( - httpContext: default, - claimsExtractor: default); - } - - [Fact] - public void GivenQueryController_WhenExecutedAction_ThenAuditLogShouldNotBeLogged() - { - var result = new NoContentResult(); - - var resultExecutedContext = new ResultExecutedContext( - new ActionContext(_httpContext, new RouteData(), new ControllerActionDescriptor() { DisplayName = "Executed Query." }), - new List(), - result, - FilterTestsHelper.CreateMockQueryController()); - - _filter.OnResultExecuted(resultExecutedContext); - - _auditHelper.DidNotReceiveWithAnyArgs().LogExecuted( - httpContext: default, - claimsExtractor: default); - } - - [Fact] - public void GivenRetrieveController_WhenExecutingAction_ThenAuditLogShouldBeLogged() - { - var actionExecutingContext = new ActionExecutingContext( - new ActionContext(_httpContext, new RouteData(), new ControllerActionDescriptor() { DisplayName = "Executing Retrieve." }), - new List(), - new Dictionary(), - FilterTestsHelper.CreateMockRetrieveController()); - - _filter.OnActionExecuting(actionExecutingContext); - - _auditHelper.Received(1).LogExecuting(_httpContext, _claimsExtractor); - } - - [Fact] - public void GivenRetrieveController_WhenExecutedActionThrowsException_ThenAuditLogShouldNotBeLogged() - { - var result = new NoContentResult(); - - var actionExecutedContext = new ActionExecutedContext( - new ActionContext(_httpContext, new RouteData(), new ControllerActionDescriptor() { DisplayName = "Executed Retrieve." }), - new List(), - FilterTestsHelper.CreateMockRetrieveController()); - - actionExecutedContext.Exception = new Exception("Test Exception."); - - _filter.OnActionExecuted(actionExecutedContext); - - _auditHelper.DidNotReceiveWithAnyArgs().LogExecuted( - httpContext: default, - claimsExtractor: default); - } - - [Fact] - public void GivenRetrieveController_WhenExecutedAction_ThenAuditLogShouldNotBeLogged() - { - var result = new NoContentResult(); - - var resultExecutedContext = new ResultExecutedContext( - new ActionContext(_httpContext, new RouteData(), new ControllerActionDescriptor() { DisplayName = "Executed Retrieve." }), - new List(), - result, - FilterTestsHelper.CreateMockRetrieveController()); - - _filter.OnResultExecuted(resultExecutedContext); - - _auditHelper.DidNotReceiveWithAnyArgs().LogExecuted( - httpContext: default, - claimsExtractor: default); - } - - [Fact] - public void GivenStoreController_WhenExecutingAction_ThenAuditLogShouldBeLogged() - { - var actionExecutingContext = new ActionExecutingContext( - new ActionContext(_httpContext, new RouteData(), new ControllerActionDescriptor() { DisplayName = "Executing Store." }), - new List(), - new Dictionary(), - FilterTestsHelper.CreateMockStoreController()); - - _filter.OnActionExecuting(actionExecutingContext); - - _auditHelper.Received(1).LogExecuting(_httpContext, _claimsExtractor); - } - - [Fact] - public void GivenStoreController_WhenExecutedActionThrowsException_ThenAuditLogShouldNotBeLogged() - { - var result = new NoContentResult(); - - var actionExecutedContext = new ActionExecutedContext( - new ActionContext(_httpContext, new RouteData(), new ControllerActionDescriptor() { DisplayName = "Executed Store." }), - new List(), - FilterTestsHelper.CreateMockStoreController()); - - actionExecutedContext.Exception = new Exception("Test Exception."); - - _filter.OnActionExecuted(actionExecutedContext); - - _auditHelper.DidNotReceiveWithAnyArgs().LogExecuted( - httpContext: default, - claimsExtractor: default); - } - - [Fact] - public void GivenStoreController_WhenExecutedAction_ThenAuditLogShouldNotBeLogged() - { - var result = new NoContentResult(); - - var resultExecutedContext = new ResultExecutedContext( - new ActionContext(_httpContext, new RouteData(), new ControllerActionDescriptor() { DisplayName = "Executed Store." }), - new List(), - result, - FilterTestsHelper.CreateMockStoreController()); - - _filter.OnResultExecuted(resultExecutedContext); - - _auditHelper.DidNotReceiveWithAnyArgs().LogExecuted( - httpContext: default, - claimsExtractor: default); - } -} diff --git a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Audit/AuditMiddlewareTests.cs b/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Audit/AuditMiddlewareTests.cs deleted file mode 100644 index b82ca775e6..0000000000 --- a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Audit/AuditMiddlewareTests.cs +++ /dev/null @@ -1,60 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Net; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.Health.Api.Features.Audit; -using Microsoft.Health.Core.Features.Security; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Api.UnitTests.Features.Audit; - -public class AuditMiddlewareTests -{ - private readonly IClaimsExtractor _claimsExtractor = Substitute.For(); - private readonly IAuditHelper _auditHelper = Substitute.For(); - - private readonly AuditMiddleware _auditMiddleware; - - private readonly HttpContext _httpContext = new DefaultHttpContext(); - - public AuditMiddlewareTests() - { - _auditMiddleware = new AuditMiddleware( - httpContext => Task.CompletedTask, - _claimsExtractor, - _auditHelper); - } - - [Fact] - public async Task GivenSuccess_WhenInvoked_ThenAuditLogShouldBeLogged() - { - _httpContext.Response.StatusCode = (int)HttpStatusCode.OK; - - await _auditMiddleware.Invoke(_httpContext); - - _auditHelper.Received(1).LogExecuted(_httpContext, _claimsExtractor, shouldCheckForAuthXFailure: true); - } - - [Theory] - [InlineData(HttpStatusCode.NotAcceptable)] - [InlineData(HttpStatusCode.BadRequest)] - [InlineData(HttpStatusCode.Conflict)] - [InlineData(HttpStatusCode.InternalServerError)] - [InlineData(HttpStatusCode.NotFound)] - [InlineData(HttpStatusCode.ServiceUnavailable)] - [InlineData(HttpStatusCode.Unauthorized)] - [InlineData(HttpStatusCode.UnsupportedMediaType)] - public async Task GivenFailure_WhenInvoked_ThenAuditLogShouldBeLogged(HttpStatusCode statusCode) - { - _httpContext.Response.StatusCode = (int)statusCode; - - await _auditMiddleware.Invoke(_httpContext); - - _auditHelper.Received(1).LogExecuted(_httpContext, _claimsExtractor, shouldCheckForAuthXFailure: true); - } -} diff --git a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/BackgroundServices/DeletedInstanceCleanupWorkerTests.cs b/src/Microsoft.Health.Dicom.Api.UnitTests/Features/BackgroundServices/DeletedInstanceCleanupWorkerTests.cs deleted file mode 100644 index 9157ba4404..0000000000 --- a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/BackgroundServices/DeletedInstanceCleanupWorkerTests.cs +++ /dev/null @@ -1,163 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Api.Features.BackgroundServices; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Delete; -using Microsoft.Health.Dicom.Core.Features.Telemetry; -using Microsoft.Health.Dicom.Core.Models.Delete; -using NSubstitute; -using OpenTelemetry; -using OpenTelemetry.Metrics; -using Xunit; - -namespace Microsoft.Health.Dicom.Api.UnitTests.Features.BackgroundServices; - -public sealed class DeletedInstanceCleanupWorkerTests : IDisposable -{ - private readonly string _meterName = Guid.NewGuid().ToString(); // Use a unique meter name to prevent conflicts with other tests - private readonly DeletedInstanceCleanupWorker _deletedInstanceCleanupWorker; - private readonly CancellationTokenSource _cancellationTokenSource = new(); - private readonly IDeleteService _deleteService = Substitute.For(); - private readonly DeleteMeter _deleteMeter; - private readonly MeterProvider _meterProvider; - private readonly List _metrics = new(); - private const int BatchSize = 10; - - public DeletedInstanceCleanupWorkerTests() - { - _meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(_meterName) - .AddInMemoryExporter(_metrics) - .Build(); - - _deleteMeter = new(_meterName, "1.0"); - DeletedInstanceCleanupConfiguration config = new() - { - BatchSize = BatchSize, - PollingInterval = TimeSpan.FromMilliseconds(100), - }; - - _deletedInstanceCleanupWorker = new DeletedInstanceCleanupWorker(_deleteService, _deleteMeter, Options.Create(config), NullLogger.Instance); - } - - [Theory] - [InlineData(0, 1)] - [InlineData(9, 1)] - [InlineData(10, 2)] - [InlineData(11, 2)] - [InlineData(19, 2)] - [InlineData(20, 3)] - [InlineData(21, 3)] - public async Task GivenANumberOfDeletedEntriesAndBatchSize_WhenCallingExecute_ThenDeleteShouldBeCalledCorrectNumberOfTimes(int numberOfDeletedInstances, int expectedDeleteCount) - { - _deleteService.CleanupDeletedInstancesAsync().ReturnsForAnyArgs( - x => GenerateCleanupDeletedInstancesAsyncResponse()); - - await _deletedInstanceCleanupWorker.ExecuteAsync(_cancellationTokenSource.Token); - await _deleteService.ReceivedWithAnyArgs(expectedDeleteCount).CleanupDeletedInstancesAsync(); - - (bool, int) GenerateCleanupDeletedInstancesAsyncResponse() - { - var returnValue = Math.Min(numberOfDeletedInstances, BatchSize); - numberOfDeletedInstances = Math.Max(numberOfDeletedInstances - BatchSize, 0); - - if (numberOfDeletedInstances == 0) - { - _cancellationTokenSource.Cancel(); - } - - return (true, returnValue); - } - } - - [Fact] - public async Task GivenANotReadyDataStore_WhenCallingExecute_ThenNothingShouldHappen() - { - int iterations = 3; - int count = 0; - _deleteService.CleanupDeletedInstancesAsync().ReturnsForAnyArgs( - x => GenerateCleanupDeletedInstancesAsyncResponse()); - - await _deletedInstanceCleanupWorker.ExecuteAsync(_cancellationTokenSource.Token); - await _deleteService.ReceivedWithAnyArgs(4).CleanupDeletedInstancesAsync(); - - (bool, int) GenerateCleanupDeletedInstancesAsyncResponse() - { - if (count < iterations) - { - count++; - throw new DataStoreNotReadyException("Datastore not ready"); - } - - _cancellationTokenSource.Cancel(); - - return (true, 1); - } - } - - [Fact] - public async Task GivenANotReadyDataStore_WhenFetchingMetrics_ThenNothingShouldHappen() - { - _deleteService - .GetMetricsAsync(_cancellationTokenSource.Token) - .Returns(x => - { - _cancellationTokenSource.Cancel(); - return Task.FromException(new DataStoreNotReadyException("Not yet!")); - }); - - await _deletedInstanceCleanupWorker.ExecuteAsync(_cancellationTokenSource.Token); - await _deleteService.Received(1).GetMetricsAsync(_cancellationTokenSource.Token); - await _deleteService.DidNotReceiveWithAnyArgs().CleanupDeletedInstancesAsync(default); - } - - [Fact] - public async Task GivenMetrics_WhenEmitting_ThenSendMetrics() - { - DateTimeOffset oldest = DateTimeOffset.UtcNow.AddDays(-5); - const int TotalExhausted = 42; - - _deleteService - .GetMetricsAsync(_cancellationTokenSource.Token) - .Returns(x => - { - _cancellationTokenSource.Cancel(); - return new DeleteMetrics { OldestDeletion = oldest, TotalExhaustedRetries = TotalExhausted }; - }); - - await _deletedInstanceCleanupWorker.ExecuteAsync(_cancellationTokenSource.Token); - - // Force the meter provides to emit the metrics earlier than they might otherwise - _meterProvider.ForceFlush(); - Assert.Equal(2, _metrics.Count); - AssertLongCounter(oldest.ToUnixTimeSeconds(), _metrics[0]); - AssertLongCounter(TotalExhausted, _metrics[1]); - } - - public void Dispose() - { - _cancellationTokenSource.Dispose(); - _meterProvider.Dispose(); - } - - private static void AssertLongCounter(long value, Metric actual) - { - Assert.Equal(MetricType.LongSum, actual.MetricType); - - MetricPointsAccessor.Enumerator t = actual.GetMetricPoints().GetEnumerator(); - - Assert.True(t.MoveNext()); - Assert.Equal(value, t.Current.GetSumLong()); - Assert.False(t.MoveNext()); - } -} diff --git a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Context/DefaultDicomRequestContext.cs b/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Context/DefaultDicomRequestContext.cs deleted file mode 100644 index f57d6c91b1..0000000000 --- a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Context/DefaultDicomRequestContext.cs +++ /dev/null @@ -1,59 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Security.Claims; -using Microsoft.Extensions.Primitives; -using Microsoft.Health.Dicom.Core.Features.Context; -using Microsoft.Health.Dicom.Core.Features.Partitioning; - -namespace Microsoft.Health.Dicom.Api.UnitTests.Features.Context; - -public class DefaultDicomRequestContext : IDicomRequestContext -{ - public Partition DataPartition { get; set; } - - public string StudyInstanceUid { get; set; } - - public string SeriesInstanceUid { get; set; } - - public string SopInstanceUid { get; set; } - - public bool IsTranscodeRequested { get; set; } - - public long BytesTranscoded { get; set; } - - public long BytesRendered { get; set; } - - public long ResponseSize { get; set; } - - public int PartCount { get; set; } - - public string Method { get; set; } - - public Uri BaseUri { get; set; } - - public Uri Uri { get; set; } - - public string CorrelationId { get; set; } - - public string RouteName { get; set; } - - public string AuditEventType { get; set; } - - public ClaimsPrincipal Principal { get; set; } - - public IDictionary RequestHeaders { get; set; } - - public IDictionary ResponseHeaders { get; set; } - - public int Version { get; set; } - - /// - /// Egress bytes from Dicom server to other resources - /// - public long TotalDicomEgressToStorageBytes { get; set; } -} diff --git a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Context/DicomRequestContextMiddlewareTests.cs b/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Context/DicomRequestContextMiddlewareTests.cs deleted file mode 100644 index 233a367087..0000000000 --- a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Context/DicomRequestContextMiddlewareTests.cs +++ /dev/null @@ -1,60 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.Health.Core.Features.Context; -using Microsoft.Health.Dicom.Api.Features.Context; -using Microsoft.Health.Dicom.Core.Features.Context; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Api.UnitTests.Features.Context; - -public class DicomRequestContextMiddlewareTests -{ - [Fact] - public async Task GivenAnHttpRequest_WhenExecutingDicomRequestContextMiddleware_ThenCorrectUriShouldBeSet() - { - IRequestContext dicomRequestContext = await SetupAsync(CreateHttpContext()); - - Assert.Equal(new Uri("https://localhost:30/studies/123"), dicomRequestContext.Uri); - } - - [Fact] - public async Task GivenAnHttpRequest_WhenExecutingDicomRequestContextMiddleware_ThenCorrectBaseUriShouldBeSet() - { - IRequestContext dicomRequestContext = await SetupAsync(CreateHttpContext()); - - Assert.Equal(new Uri("https://localhost:30/studies"), dicomRequestContext.BaseUri); - } - - private static async Task SetupAsync(HttpContext httpContext) - { - var dicomRequestContextAccessor = Substitute.For(); - var dicomContextMiddleware = new DicomRequestContextMiddleware(next: (innerHttpContext) => Task.CompletedTask); - - await dicomContextMiddleware.Invoke(httpContext, dicomRequestContextAccessor); - - Assert.NotNull(dicomRequestContextAccessor.RequestContext); - - return dicomRequestContextAccessor.RequestContext; - } - - private static HttpContext CreateHttpContext() - { - HttpContext httpContext = new DefaultHttpContext(); - - httpContext.Request.Method = "GET"; - httpContext.Request.Scheme = "https"; - httpContext.Request.Host = new HostString("localhost", 30); - httpContext.Request.PathBase = new PathString("/studies"); - httpContext.Request.Path = new PathString("/123"); - - return httpContext; - } - -} diff --git a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Conventions/ApiVersionsConventionTests.cs b/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Conventions/ApiVersionsConventionTests.cs deleted file mode 100644 index 5002f25e6e..0000000000 --- a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Conventions/ApiVersionsConventionTests.cs +++ /dev/null @@ -1,129 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Globalization; -using System.Linq; -using System.Reflection; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ApplicationModels; -using Microsoft.AspNetCore.Mvc.Versioning.Conventions; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Api.Features.Conventions; -using Microsoft.Health.Dicom.Core.Configs; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Api.UnitTests.Features.Conventions; - -public class ApiVersionsConventionTests -{ - [Fact] - public void GivenController_NoVersionAttribute_AllSupportedVersionsApplied() - { - // arrange - var controllerModel = new ControllerModel(typeof(TestController).GetTypeInfo(), Array.Empty()); - var controller = Substitute.For(); - var featuresOptions = Options.Create(new FeatureConfiguration()); - var convention = new ApiVersionsConvention(featuresOptions); - - // act - convention.Apply(controller, controllerModel); - - // assert - var supportedVersions = ImmutableHashSet.Create(new ApiVersion(1, 0, "prerelease"), new ApiVersion(1, 0)); - controller.Received().HasApiVersions(supportedVersions); - } - - [Fact] - public void GivenController_IntroducedInAttribute_CorrectVersionsApplied() - { - // arrange - var attributes = new object[] { new IntroducedInApiVersionAttribute(1) }; - var controllerModel = new ControllerModel(typeof(TestController).GetTypeInfo(), attributes); - var controller = Substitute.For(); - var featuresOptions = Options.Create(new FeatureConfiguration()); - var convention = new ApiVersionsConvention(featuresOptions); - - // act - convention.Apply(controller, controllerModel); - - // assert - var supportedVersions = ImmutableHashSet.Create(new ApiVersion(1, 0)); - controller.Received().HasApiVersions(supportedVersions); - } - - [Fact] - public void GivenController_IntroducedInAttribute_WhenNewApiTurnedOnCorrectVersionsApplied() - { - // arrange - var attributes = new object[] { new IntroducedInApiVersionAttribute(1) }; - var controllerModel = new ControllerModel(typeof(TestController).GetTypeInfo(), attributes); - var controller = Substitute.For(); - var featuresOptions = Options.Create(new FeatureConfiguration { EnableLatestApiVersion = true }); - var convention = new ApiVersionsConvention(featuresOptions); - - // act - convention.Apply(controller, controllerModel); - - // assert - var supportedVersions = ImmutableHashSet.Create(new ApiVersion(1, 0), new ApiVersion(2, 0)); - controller.Received().HasApiVersions(supportedVersions); - } - - [Fact] - public void GivenController_NoVersionAttribute_WhenNewApiTurnedOnCorrectVersionsApplied() - { - // arrange - var controllerModel = new ControllerModel(typeof(TestController).GetTypeInfo(), Array.Empty()); - var controller = Substitute.For(); - var featuresOptions = Options.Create(new FeatureConfiguration { EnableLatestApiVersion = true }); - var convention = new ApiVersionsConvention(featuresOptions); - - // act - convention.Apply(controller, controllerModel); - - // assert - var supportedVersions = ImmutableHashSet.Create(new ApiVersion(1, 0, "prerelease"), new ApiVersion(1, 0), new ApiVersion(2, 0)); - controller.Received().HasApiVersions(supportedVersions); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public void GivenActionInLatest_WhenLatestConfigured_ThenAddOrRemove(bool enableLatest) - { - // arrange - MethodInfo actionMethod = typeof(TestController).GetMethod(nameof(TestController.GetResultAsync)); - var controllerModel = new ControllerModel(typeof(TestController).GetTypeInfo(), Array.Empty()) - { - Actions = { new ActionModel(actionMethod, actionMethod.GetCustomAttributes().ToList()) }, - }; - var builder = new ControllerApiVersionConventionBuilder(typeof(TestController)); - var featuresOptions = Options.Create(new FeatureConfiguration { EnableLatestApiVersion = enableLatest }); - var convention = new ApiVersionsConvention(featuresOptions); - int nextVersion = ApiVersionsConvention.CurrentVersion + 1; - ApiVersionsConvention.UpcomingVersion = new List() { ApiVersion.Parse(nextVersion.ToString(CultureInfo.InvariantCulture)) }; - - // act - convention.Apply(builder, controllerModel); - - // assert - if (enableLatest) - Assert.Single(controllerModel.Actions); - else - Assert.Empty(controllerModel.Actions); - } - - private sealed class TestController : ControllerBase - { - [MapToApiVersion("3.0")] - public Task GetResultAsync() - => throw new NotImplementedException(); - } -} diff --git a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Converter/CustomJsonStringEnumConverterTests.cs b/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Converter/CustomJsonStringEnumConverterTests.cs deleted file mode 100644 index a834af2f7a..0000000000 --- a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Converter/CustomJsonStringEnumConverterTests.cs +++ /dev/null @@ -1,53 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Text.Json; -using Microsoft.Health.Dicom.Api.Features.Converter; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Xunit; - -namespace Microsoft.Health.Dicom.Api.UnitTests.Features.Converter; - -public class CustomJsonStringEnumConverterTests -{ - private readonly JsonSerializerOptions _options; - public CustomJsonStringEnumConverterTests() - { - _options = new JsonSerializerOptions(); - _options.Converters.Add(new EnumNameJsonConverter()); - } - - [Theory] - [InlineData("\"Instance\"", QueryTagLevel.Instance)] - [InlineData("\"instance\"", QueryTagLevel.Instance)] // lower case - [InlineData("\"0\"", QueryTagLevel.Instance)] // string number -- Enum.TryParse support this by default. - public void GivenValidEnumString_WhenRead_ThenShouldReturnExpectedValue(string value, QueryTagLevel expected) - { - Assert.Equal(expected, JsonSerializer.Deserialize(value, typeof(QueryTagLevel), _options)); - } - - [Theory] - [InlineData("\"Enab\"")] // invalid string value - [InlineData("1")] // number - public void GivenInvalidEnumString_WhenRead_ThenShouldReturnExpectedValue(string value) - { - var exp = Assert.Throws(() => JsonSerializer.Deserialize(value, typeof(QueryTagLevel), _options)); - Assert.Equal("The value is not valid. It need to be one of \"Instance\",\"Series\",\"Study\".", exp.Message); - } - - [Fact] - public void GivenNumber_WhenRead_ThenShouldReturnExpectedValue() - { - var exp = Assert.Throws(() => JsonSerializer.Deserialize("1", typeof(QueryTagLevel), _options)); - Assert.Equal("The value is not valid. It need to be one of \"Instance\",\"Series\",\"Study\".", exp.Message); - } - - [Fact] - public void GivenValidEnum_WhenWrite_ThenShouldWriteExpectedValue() - { - var actual = JsonSerializer.Serialize(QueryTagLevel.Instance, typeof(QueryTagLevel), _options); - Assert.Equal("\"Instance\"", actual); - } -} diff --git a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Exceptions/ExceptionHandlingMiddlewareTests.cs b/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Exceptions/ExceptionHandlingMiddlewareTests.cs deleted file mode 100644 index 4387521028..0000000000 --- a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Exceptions/ExceptionHandlingMiddlewareTests.cs +++ /dev/null @@ -1,177 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Text.Json; -using System.Threading.Tasks; -using Azure; -using Microsoft.AspNetCore.Connections; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Health.Abstractions.Exceptions; -using Microsoft.Health.Api.Features.Audit; -using Microsoft.Health.Dicom.Api.Features.Exceptions; -using Microsoft.Health.Dicom.Core.Exceptions; -using NSubstitute; -using Xunit; -using NotSupportedException = Microsoft.Health.Dicom.Core.Exceptions.NotSupportedException; - -namespace Microsoft.Health.Dicom.Api.UnitTests.Features.Exceptions; - -public class ExceptionHandlingMiddlewareTests -{ - private readonly DefaultHttpContext _context; - - public ExceptionHandlingMiddlewareTests() - { - _context = new DefaultHttpContext(); - } - - public static IEnumerable GetExceptionToStatusCodeMapping() - { - yield return new object[] { new CustomValidationException(), HttpStatusCode.BadRequest }; - yield return new object[] { new ArgumentException(), HttpStatusCode.BadRequest }; - yield return new object[] { new System.ComponentModel.DataAnnotations.ValidationException(), HttpStatusCode.BadRequest }; - yield return new object[] { new NotSupportedException("Not supported."), HttpStatusCode.BadRequest }; - yield return new object[] { new AuditHeaderCountExceededException(AuditConstants.MaximumNumberOfCustomHeaders + 1), HttpStatusCode.BadRequest }; - yield return new object[] { new AuditHeaderTooLargeException("TestHeader", AuditConstants.MaximumLengthOfCustomHeader + 1), HttpStatusCode.BadRequest }; - yield return new object[] { new ResourceNotFoundException("Resource not found."), HttpStatusCode.NotFound }; - yield return new object[] { new TranscodingException(), HttpStatusCode.NotAcceptable }; - yield return new object[] { new DicomImageException(), HttpStatusCode.NotAcceptable }; - yield return new object[] { new DataStoreException(new TaskCanceledException()), HttpStatusCode.BadRequest }; - yield return new object[] { new DataStoreException("Something went wrong."), HttpStatusCode.ServiceUnavailable }; - yield return new object[] { new InstanceAlreadyExistsException(), HttpStatusCode.Conflict }; - yield return new object[] { new UnsupportedMediaTypeException("Media type is not supported."), HttpStatusCode.UnsupportedMediaType }; - yield return new object[] { new ServiceUnavailableException(), HttpStatusCode.ServiceUnavailable }; - yield return new object[] { new ItemNotFoundException(new Exception()), HttpStatusCode.InternalServerError }; - yield return new object[] { new CustomServerException(), HttpStatusCode.ServiceUnavailable }; - yield return new object[] { new BadHttpRequestException("Something bad happened!"), HttpStatusCode.BadRequest }; - yield return new object[] { new IOException("The request stream was aborted."), HttpStatusCode.BadRequest }; - yield return new object[] { new ConnectionResetException(string.Empty), HttpStatusCode.BadRequest }; - yield return new object[] { new OperationCanceledException(), HttpStatusCode.BadRequest }; - yield return new object[] { new TaskCanceledException(), HttpStatusCode.BadRequest }; - yield return new object[] { new InvalidOperationException(), HttpStatusCode.BadRequest }; - yield return new object[] { new PayloadTooLargeException(1), HttpStatusCode.RequestEntityTooLarge }; - yield return new object[] { new DataStoreException(new Exception(), isExternal: true), HttpStatusCode.FailedDependency }; - yield return new object[] { new DataStoreRequestFailedException(new RequestFailedException(String.Empty), isExternal: true), HttpStatusCode.FailedDependency }; - yield return new object[] { new RequestFailedException(403, "The key vault key is not found to unwrap the encryption key.", "KeyVaultEncryptionKeyNotFound", new Exception()), HttpStatusCode.FailedDependency }; - yield return new object[] { new DataStoreException(new RequestFailedException(403, "The key vault key is not found to unwrap the encryption key.", "KeyVaultEncryptionKeyNotFound", new Exception())), HttpStatusCode.FailedDependency }; - yield return new object[] { new DataStoreRequestFailedException(new RequestFailedException(403, "The key vault key is not found to unwrap the encryption key.", "KeyVaultEncryptionKeyNotFound", new Exception())), HttpStatusCode.FailedDependency }; - yield return new object[] { new RequestFailedException(403, "The key vault key is not found to unwrap the encryption key.", "KeyVaultEncryptionKeyNotFound", new Exception()), HttpStatusCode.FailedDependency }; - yield return new object[] { new DataStoreException("Something went wrong.", new TaskCanceledException()), HttpStatusCode.BadRequest }; - yield return new object[] { new DataStoreException("Something went wrong.", new OperationCanceledException()), HttpStatusCode.BadRequest }; - yield return new object[] { new MicrosoftHealthException("Something went wrong.", new OperationCanceledException()), HttpStatusCode.BadRequest }; - } - - [Theory] - [MemberData(nameof(GetExceptionToStatusCodeMapping))] - public async Task GivenAnException_WhenMiddlewareIsExecuted_ThenCorrectStatusCodeShouldBeReturned(Exception exception, HttpStatusCode expectedStatusCode) - { - ExceptionHandlingMiddleware baseExceptionMiddleware = CreateExceptionHandlingMiddleware(innerHttpContext => throw exception); - - baseExceptionMiddleware.ExecuteResultAsync(Arg.Any(), Arg.Any()).Returns(Task.CompletedTask); - - await baseExceptionMiddleware.Invoke(_context); - - await baseExceptionMiddleware - .Received() - .ExecuteResultAsync( - Arg.Any(), - Arg.Is(x => x.StatusCode == (int)expectedStatusCode)); - } - - [Fact] - public async Task GivenAnInternalServerException_WhenMiddlewareIsExecuted_ThenMessageShouldBeOverwritten() - { - ExceptionHandlingMiddleware baseExceptionMiddleware = CreateExceptionHandlingMiddleware(innerHttpContext => throw new Exception("Unhandled exception.")); - - baseExceptionMiddleware.ExecuteResultAsync(Arg.Any(), Arg.Any()).Returns(Task.CompletedTask); - - await baseExceptionMiddleware.Invoke(_context); - - await baseExceptionMiddleware - .Received() - .ExecuteResultAsync( - Arg.Any(), - Arg.Is(x => x.Content == DicomApiResource.InternalServerError)); - } - - [Fact] - public async Task GivenAJsonException_WhenMiddlewareIsExecuted_ThenMessageShouldBeOverwritten() - { - ExceptionHandlingMiddleware baseExceptionMiddleware = CreateExceptionHandlingMiddleware(innerHttpContext => throw new JsonException("Parsing data.")); - - baseExceptionMiddleware.ExecuteResultAsync(Arg.Any(), Arg.Any()).Returns(Task.CompletedTask); - - await baseExceptionMiddleware.Invoke(_context); - - await baseExceptionMiddleware - .Received() - .ExecuteResultAsync( - Arg.Any(), - Arg.Is(x => x.Content == DicomApiResource.InvalidSyntax)); - } - - [Fact] - public async Task WhenExecutingExceptionMiddleware_GivenAnHttpContextWithNoException_TheResponseShouldBeEmpty() - { - ExceptionHandlingMiddleware baseExceptionMiddleware = CreateExceptionHandlingMiddleware(innerHttpContext => Task.CompletedTask); - - await baseExceptionMiddleware.Invoke(_context); - - Assert.Equal(200, _context.Response.StatusCode); - Assert.Null(_context.Response.ContentType); - Assert.Equal(0, _context.Response.Body.Length); - } - - [Fact] - public async Task GivenAnAggregateExceptionHasTaskCanceled_WhenMiddlewareIsExecuted_ThenMessageShouldBeOverwritten() - { - var innerExceptions = new List - { - new TaskCanceledException("Operation canceled"), - new ServiceUnavailableException() - }; - - var aggException = new AggregateException(innerExceptions); - - ExceptionHandlingMiddleware baseExceptionMiddleware = CreateExceptionHandlingMiddleware(innerHttpContext => throw new DataStoreException(aggException)); - - baseExceptionMiddleware.ExecuteResultAsync(Arg.Any(), Arg.Any()).Returns(Task.CompletedTask); - - await baseExceptionMiddleware.Invoke(_context); - - await baseExceptionMiddleware - .Received() - .ExecuteResultAsync( - Arg.Any(), - Arg.Is(x => x.StatusCode.Value == (int)HttpStatusCode.BadRequest)); - } - - private static ExceptionHandlingMiddleware CreateExceptionHandlingMiddleware(RequestDelegate nextDelegate) - { - return Substitute.ForPartsOf(nextDelegate, NullLogger.Instance); - } - - private class CustomValidationException : ValidationException - { - public CustomValidationException() - : base("Validation exception.") - { - } - } - - private class CustomServerException : DicomServerException - { - public CustomServerException() - : base("Server exception.") - { - } - } -} diff --git a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Filters/AcceptContentFilterAttributeTests.cs b/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Filters/AcceptContentFilterAttributeTests.cs deleted file mode 100644 index 1533c67884..0000000000 --- a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Filters/AcceptContentFilterAttributeTests.cs +++ /dev/null @@ -1,153 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Net; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Abstractions; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.Routing; -using Microsoft.Health.Dicom.Api.Features.Filters; -using Microsoft.Health.Dicom.Core.Web; -using Microsoft.Net.Http.Headers; -using Xunit; - -namespace Microsoft.Health.Dicom.Api.UnitTests.Features.Filters; - -/// -/// These tests leverage the existing ASP.NET Core tests at: https://github.com/dotnet/aspnetcore/blob/main/src/Http/Headers/test/MediaTypeHeaderValueTest.cs -/// -public class AcceptContentFilterAttributeTests -{ - private AcceptContentFilterAttribute _filter; - private readonly ActionExecutingContext _context; - - public AcceptContentFilterAttributeTests() - { - _context = CreateContext(); - } - - [Fact] - public void GivenARequestWithNoAcceptHeader_ThenNotAcceptableStatusCodeShouldBeReturned() - { - _filter = CreateFilter([KnownContentTypes.ApplicationDicom, KnownContentTypes.ApplicationOctetStream]); - - _filter.OnActionExecuting(_context); - - Assert.Equal((int)HttpStatusCode.NotAcceptable, (_context.Result as StatusCodeResult)?.StatusCode); - } - - [Theory] - [InlineData("*/*")] - [InlineData("application/*")] - [InlineData("application/json")] - [InlineData("application/xml")] - [InlineData("application/dicom+json")] - [InlineData("applicAtion/dICOM+Json")] - [InlineData("application/dicom+xml")] - [InlineData("applicAtion/DICOM+XmL")] - [InlineData("application/dicom+json; transfer-syntax=*")] - [InlineData("application/dicom+json; transfer-syntax=\"*\"")] - public void GivenARequestWithAValidAcceptHeader_WhenMediaTypeMatches_ThenSuccess(string acceptHeaderMediaType) - { - _filter = CreateFilter([KnownContentTypes.ApplicationDicomJson, "application/dicom+xml"]); - - _context.HttpContext.Request.Headers.TryAdd(HeaderNames.Accept, acceptHeaderMediaType); - - _filter.OnActionExecuting(_context); - - Assert.Null((_context.Result as StatusCodeResult)?.StatusCode); - } - - [Theory] - [InlineData("application/dicom+json+something")] - [InlineData("application/dicom")] - [InlineData("")] - [InlineData(null)] - public void GivenARequestWithAValidAcceptHeader_WhenMediaTypeDoesntMatch_ThenFailure(string acceptHeaderMediaType) - { - _filter = CreateFilter([KnownContentTypes.ApplicationDicomJson, "application/dicom+xml"]); - - _context.HttpContext.Request.Headers.Append(HeaderNames.Accept, acceptHeaderMediaType); - - _filter.OnActionExecuting(_context); - - Assert.Equal((int)HttpStatusCode.NotAcceptable, (_context.Result as StatusCodeResult)?.StatusCode); - } - - [Theory] - [InlineData("application/dicom+json", "image/jpg")] - [InlineData("application/dicom+xml", "image/png")] - [InlineData("application/dicom+json", "application/dicom+xml")] - [InlineData("image/png", "application/dicom+xml")] - [InlineData("application/dicom", "application/xml")] - public void GivenARequestWithMultipleAcceptHeaders_WhenAnyMediaTypeMatches_ThenSuccess(params string[] acceptHeaderMediaType) - { - _filter = CreateFilter([KnownContentTypes.ApplicationDicomJson, "application/dicom+xml"]); - - _context.HttpContext.Request.Headers.Append(HeaderNames.Accept, acceptHeaderMediaType); - - _filter.OnActionExecuting(_context); - - Assert.Null((_context.Result as StatusCodeResult)?.StatusCode); - } - - [Theory] - [InlineData("image/png", "image/jpg")] - [InlineData("application/dicom", "image/png")] - public void GivenARequestWithMultipleAcceptHeaders_WhenNoMediaTypeMatches_ThenFailure(params string[] acceptHeaderMediaType) - { - _filter = CreateFilter([KnownContentTypes.ApplicationDicomJson, "application/dicom+xml"]); - - _context.HttpContext.Request.Headers.Append(HeaderNames.Accept, acceptHeaderMediaType); - - _filter.OnActionExecuting(_context); - - Assert.Equal((int)HttpStatusCode.NotAcceptable, (_context.Result as StatusCodeResult)?.StatusCode); - } - - [Theory] - [InlineData("image/png, image/jpg, application/dicom+xml")] - [InlineData("application/dicom+json, application/xml")] - public void GivenARequestWithOneAcceptHeaderWithMultipleTypes_WhenAnyMediaTypeMatches_ThenSuccess(string acceptHeaderMediaType) - { - _filter = CreateFilter([KnownContentTypes.ApplicationDicomJson, "application/dicom+xml"]); - - _context.HttpContext.Request.Headers.Append(HeaderNames.Accept, acceptHeaderMediaType); - - _filter.OnActionExecuting(_context); - - Assert.Null((_context.Result as StatusCodeResult)?.StatusCode); - } - - [Theory] - [InlineData("image/png, image/jpg, application/dicom")] - [InlineData("application/dicom, application/pdf")] - public void GivenARequestWithOneAcceptHeaderWithMultipleTypes_WhenNoMediaTypeMatches_ThenFailure(string acceptHeaderMediaType) - { - _filter = CreateFilter([KnownContentTypes.ApplicationDicomJson, "application/dicom+xml"]); - - _context.HttpContext.Request.Headers.Append(HeaderNames.Accept, acceptHeaderMediaType); - - _filter.OnActionExecuting(_context); - - Assert.Equal((int)HttpStatusCode.NotAcceptable, (_context.Result as StatusCodeResult)?.StatusCode); - } - - private static AcceptContentFilterAttribute CreateFilter(string[] supportedMediaTypes) - { - return new AcceptContentFilterAttribute(supportedMediaTypes); - } - - private static ActionExecutingContext CreateContext() - { - return new ActionExecutingContext( - new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()), - new List(), - new Dictionary(), - null); - } -} diff --git a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Filters/AcceptTransferSyntaxFilterAttributeTests.cs b/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Filters/AcceptTransferSyntaxFilterAttributeTests.cs deleted file mode 100644 index 8338300abd..0000000000 --- a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Filters/AcceptTransferSyntaxFilterAttributeTests.cs +++ /dev/null @@ -1,109 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Net; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Abstractions; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.AspNetCore.Routing; -using Microsoft.Health.Dicom.Api.Features.Filters; -using Xunit; - -namespace Microsoft.Health.Dicom.Api.UnitTests.Features.Filters; - -public class AcceptTransferSyntaxFilterAttributeTests -{ - private const string DefaultTransferSyntax = "*"; - private const string TransferSyntaxHeaderPrefix = "transfer-syntax"; - private AcceptTransferSyntaxFilterAttribute _filter; - private readonly ActionExecutingContext _context; - - public AcceptTransferSyntaxFilterAttributeTests() - { - _context = CreateContext(); - } - - [Theory] - [InlineData("hello")] - [InlineData("1.2.840.10008.1.2.4.50")] - public void GivenARequestWithUnsupportedTransferSyntax_WhenValidatingTheTransferSyntaxAgainstTransferSyntaxFilter_ThenCorrectStatusCodeShouldBeReturned(string parsedTransferSyntax) - { - _filter = CreateFilter(new[] { DefaultTransferSyntax }); - _context.ModelState.SetModelValue(TransferSyntaxHeaderPrefix, new ValueProviderResult(parsedTransferSyntax)); - - _filter.OnActionExecuting(_context); - - Assert.Equal((int)HttpStatusCode.NotAcceptable, (_context.Result as StatusCodeResult)?.StatusCode); - } - - [Theory] - [InlineData("*")] - public void GivenARequestWithSupportedTransferSyntax_WhenValidatingTheTransferSyntaxAgainstTransferSyntaxFilter_ThenFilterShouldPass(string parsedTransferSyntax) - { - _filter = CreateFilter(new[] { DefaultTransferSyntax }); - _context.ModelState.SetModelValue(TransferSyntaxHeaderPrefix, new ValueProviderResult(parsedTransferSyntax)); - - _filter.OnActionExecuting(_context); - - Assert.Null((_context.Result as StatusCodeResult)?.StatusCode); - } - - [Fact] - public void GivenARequestWithNoTransferSyntaxValue_WhenValidatingTheTransferSyntaxAgainstTransferSyntaxFilter_ThenNotAcceptableStatusCodeShouldBeReturned() - { - _filter = CreateFilter(new[] { DefaultTransferSyntax }); - _context.ModelState.SetModelValue(TransferSyntaxHeaderPrefix, null, null); - - _filter.OnActionExecuting(_context); - - Assert.Equal((int)HttpStatusCode.NotAcceptable, (_context.Result as StatusCodeResult)?.StatusCode); - } - - [Fact] - public void GivenARequestWithNoTransferSyntaxValue_WhenValidatingTheTransferSyntaxAgainstTransferSyntaxFilterWhichAllowMissing_ThenFilterShouldPass() - { - _filter = CreateFilter(new[] { DefaultTransferSyntax }, allowMissing: true); - _context.ModelState.SetModelValue(TransferSyntaxHeaderPrefix, null, null); - - _filter.OnActionExecuting(_context); - - Assert.Null((_context.Result as StatusCodeResult)?.StatusCode); - } - - [Fact] - public void GivenARequestWithNoSetTransferSyntaxHeader_WhenValidatingTheTransferSyntaxAgainstTransferSyntaxFilter_ThenNotAcceptableStatusCodeShouldBeReturned() - { - _filter = CreateFilter(new[] { DefaultTransferSyntax }); - _filter.OnActionExecuting(_context); - - Assert.Equal((int)HttpStatusCode.NotAcceptable, (_context.Result as StatusCodeResult)?.StatusCode); - } - - [Fact] - public void GivenARequestWithNoSetTransferSyntaxHeader_WhenValidatingTheTransferSyntaxAgainstTransferSyntaxFilterWhichAllowMissing_ThenFilterShouldPass() - { - _filter = CreateFilter(new[] { DefaultTransferSyntax }, allowMissing: true); - _filter.OnActionExecuting(_context); - - Assert.Null((_context.Result as StatusCodeResult)?.StatusCode); - } - - private static AcceptTransferSyntaxFilterAttribute CreateFilter(string[] supportedTransferSyntaxes, bool allowMissing = false) - { - return new AcceptTransferSyntaxFilterAttribute(supportedTransferSyntaxes, allowMissing); - } - - private static ActionExecutingContext CreateContext() - { - return new ActionExecutingContext( - new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()), - new List(), - new Dictionary(), - null); - } -} diff --git a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Filters/BodyModelStateValidatorAttributeTests.cs b/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Filters/BodyModelStateValidatorAttributeTests.cs deleted file mode 100644 index 60bc35b4a1..0000000000 --- a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Filters/BodyModelStateValidatorAttributeTests.cs +++ /dev/null @@ -1,56 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Abstractions; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.AspNetCore.Routing; -using Microsoft.Health.Dicom.Api.Features.Filters; -using Microsoft.Health.Dicom.Api.Web; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Api.UnitTests.Features.Filters; - -public class BodyModelStateValidatorAttributeTests -{ - private readonly BodyModelStateValidatorAttribute _filter; - private readonly ActionExecutingContext _context; - - public BodyModelStateValidatorAttributeTests() - { - _context = CreateContext(); - _filter = new BodyModelStateValidatorAttribute(); - } - - [Fact] - public void GivenModelError_WhenOnActionExecution_ThenShouldThrowInvalidRequestBodyException() - { - string key1 = "key1"; - string key2 = "key2"; - string key3 = "key3"; - string error1 = "error1"; - string error2 = "error2"; - string error3 = "error3"; - _context.ModelState.SetModelValue(key1, new ValueProviderResult("world")); - _context.ModelState.AddModelError(key2, error1); - _context.ModelState.AddModelError(key2, error2); - _context.ModelState.AddModelError(key3, error3); - var exp = Assert.Throws(() => _filter.OnActionExecuting(_context)); - Assert.Equal($"The field '{key3}' in request body is invalid: {error3}", exp.Message); - } - - private static ActionExecutingContext CreateContext() - { - return Substitute.For( - new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()), - new List(), - new Dictionary(), - null); - } -} diff --git a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Filters/DicomRequestContextDataPopulatingFilterAttributeTests.cs b/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Filters/DicomRequestContextDataPopulatingFilterAttributeTests.cs deleted file mode 100644 index 5a8b10ef1b..0000000000 --- a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Filters/DicomRequestContextDataPopulatingFilterAttributeTests.cs +++ /dev/null @@ -1,140 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Controllers; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.Mvc.Routing; -using Microsoft.AspNetCore.Routing; -using Microsoft.Health.Api.Features.Audit; -using Microsoft.Health.Dicom.Api.Features.Filters; -using Microsoft.Health.Dicom.Api.Features.Routing; -using Microsoft.Health.Dicom.Api.UnitTests.Features.Context; -using Microsoft.Health.Dicom.Core.Features.Context; -using Microsoft.Health.Dicom.Core.Messages; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Api.UnitTests.Features.Filters; - -public class DicomRequestContextDataPopulatingFilterAttributeTests -{ - private readonly ControllerActionDescriptor _controllerActionDescriptor; - private readonly IDicomRequestContextAccessor _dicomRequestContextAccessor = Substitute.For(); - private readonly DefaultDicomRequestContext _dicomRequestContext = new DefaultDicomRequestContext(); - private readonly IAuditEventTypeMapping _auditEventTypeMapping = Substitute.For(); - private readonly HttpContext _httpContext = new DefaultHttpContext(); - private readonly ActionExecutingContext _actionExecutingContext; - private const string ControllerName = "controller"; - private const string ActionName = "actionName"; - private const string RouteName = "routeName"; - private const string NormalAuditEventType = "event-name"; - private const string StudyInstanceUid = "123"; - private const string SeriesInstanceUid = "456"; - private const string SopInstanceUid = "789"; - - private readonly DicomRequestContextRouteDataPopulatingFilterAttribute _filterAttribute; - - public DicomRequestContextDataPopulatingFilterAttributeTests() - { - _controllerActionDescriptor = new ControllerActionDescriptor - { - DisplayName = "Executing Context Test Descriptor", - ActionName = ActionName, - ControllerName = ControllerName, - AttributeRouteInfo = new AttributeRouteInfo - { - Name = RouteName, - }, - }; - - _actionExecutingContext = new ActionExecutingContext( - new ActionContext(_httpContext, new RouteData(), _controllerActionDescriptor), - new List(), - new Dictionary(), - FilterTestsHelper.CreateMockRetrieveController()); - - _dicomRequestContextAccessor.RequestContext.Returns(_dicomRequestContext); - - _filterAttribute = new DicomRequestContextRouteDataPopulatingFilterAttribute(_dicomRequestContextAccessor, _auditEventTypeMapping); - } - - [Fact] - public void GivenRetrieveRequestForStudy_WhenExecutingAnAction_ThenValuesShouldBeSetOnDicomFhirRequestContext() - { - var routeValueDictionary = new RouteValueDictionary - { - { KnownActionParameterNames.StudyInstanceUid, "123" }, - }; - _actionExecutingContext.RouteData = new RouteData(routeValueDictionary); - - ExecuteAndValidateFilter(NormalAuditEventType, NormalAuditEventType, ResourceType.Study); - } - - [Fact] - public void GivenRetrieveRequestForSeries_WhenExecutingAnAction_ThenValuesShouldBeSetOnDicomFhirRequestContext() - { - var routeValueDictionary = new RouteValueDictionary - { - { KnownActionParameterNames.StudyInstanceUid, StudyInstanceUid }, - { KnownActionParameterNames.SeriesInstanceUid, SeriesInstanceUid }, - }; - _actionExecutingContext.RouteData = new RouteData(routeValueDictionary); - - ExecuteAndValidateFilter(NormalAuditEventType, NormalAuditEventType, ResourceType.Series); - } - - [Fact] - public void GivenRetrieveRequestForSopInstance_WhenExecutingAnAction_ThenValuesShouldBeSetOnDicomFhirRequestContext() - { - var routeValueDictionary = new RouteValueDictionary - { - { KnownActionParameterNames.StudyInstanceUid, StudyInstanceUid }, - { KnownActionParameterNames.SeriesInstanceUid, SeriesInstanceUid }, - { KnownActionParameterNames.SopInstanceUid, SopInstanceUid }, - }; - _actionExecutingContext.RouteData = new RouteData(routeValueDictionary); - - ExecuteAndValidateFilter(NormalAuditEventType, NormalAuditEventType, ResourceType.Series); - } - - private void ExecuteAndValidateFilter( - string auditEventTypeFromMapping, - string expectedAuditEventType, - ResourceType? resourceType = null) - { - _auditEventTypeMapping.GetAuditEventType(ControllerName, ActionName).Returns(auditEventTypeFromMapping); - - _filterAttribute.OnActionExecuting(_actionExecutingContext); - - Assert.NotNull(_dicomRequestContextAccessor.RequestContext.AuditEventType); - Assert.Equal(expectedAuditEventType, _dicomRequestContextAccessor.RequestContext.AuditEventType); - Assert.Equal(RouteName, _dicomRequestContextAccessor.RequestContext.RouteName); - - if (resourceType != null) - { - switch (resourceType) - { - case ResourceType.Study: - Assert.Equal(StudyInstanceUid, _dicomRequestContextAccessor.RequestContext.StudyInstanceUid); - break; - case ResourceType.Series: - Assert.Equal(StudyInstanceUid, _dicomRequestContextAccessor.RequestContext.StudyInstanceUid); - Assert.Equal(SeriesInstanceUid, _dicomRequestContextAccessor.RequestContext.SeriesInstanceUid); - break; - case ResourceType.Instance: - case ResourceType.Frames: - Assert.Equal(StudyInstanceUid, _dicomRequestContextAccessor.RequestContext.StudyInstanceUid); - Assert.Equal(SeriesInstanceUid, _dicomRequestContextAccessor.RequestContext.SeriesInstanceUid); - Assert.Equal(SopInstanceUid, _dicomRequestContextAccessor.RequestContext.SopInstanceUid); - break; - default: - break; - } - } - } -} diff --git a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Filters/FilterTestsHelper.cs b/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Filters/FilterTestsHelper.cs deleted file mode 100644 index 3a8ef56819..0000000000 --- a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Filters/FilterTestsHelper.cs +++ /dev/null @@ -1,42 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Api.Controllers; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Tests.Common; -using NSubstitute; - -namespace Microsoft.Health.Dicom.Api.UnitTests.Features.Filters; - -public static class FilterTestsHelper -{ - public static ChangeFeedController CreateMockChangeFeedController() - { - return Mock.TypeWithArguments(Options.Create(new FeatureConfiguration())); - } - - public static DeleteController CreateMockDeleteController() - { - return Mock.TypeWithArguments(Options.Create(new FeatureConfiguration())); - } - - public static QueryController CreateMockQueryController() - { - return Mock.TypeWithArguments(Options.Create(new FeatureConfiguration())); - } - - public static RetrieveController CreateMockRetrieveController() - { - var retrieveConfigSnapshot = Substitute.For>(); - retrieveConfigSnapshot.Value.Returns(new RetrieveConfiguration()); - return Mock.TypeWithArguments(Options.Create(new FeatureConfiguration()), retrieveConfigSnapshot); - } - - public static StoreController CreateMockStoreController() - { - return Mock.TypeWithArguments(Options.Create(new FeatureConfiguration())); - } -} diff --git a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Filters/PopulateDataPartitionFilterAttributeTests.cs b/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Filters/PopulateDataPartitionFilterAttributeTests.cs deleted file mode 100644 index 86d68ffd1e..0000000000 --- a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Filters/PopulateDataPartitionFilterAttributeTests.cs +++ /dev/null @@ -1,172 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Threading.Tasks; -using MediatR; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Controllers; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.Mvc.Routing; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Api.Features.Filters; -using Microsoft.Health.Dicom.Api.Features.Routing; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Context; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Core.Messages.Partitioning; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Api.UnitTests.Features.Filters; - -public class PopulateDataPartitionFilterAttributeTests -{ - private readonly ControllerActionDescriptor _controllerActionDescriptor; - private readonly HttpContext _httpContext; - private readonly ActionExecutingContext _actionExecutingContext; - private readonly IDicomRequestContextAccessor _dicomRequestContextAccessor; - private readonly IMediator _mediator; - private readonly IOptions _featureConfiguration; - private readonly ActionExecutionDelegate _nextActionDelegate; - - private const string ControllerName = "controller"; - private const string ActionName = "actionName"; - private const string RouteName = "routeName"; - - private PopulateDataPartitionFilterAttribute _filterAttribute; - - public PopulateDataPartitionFilterAttributeTests() - { - _controllerActionDescriptor = new ControllerActionDescriptor - { - DisplayName = "Executing Context Test Descriptor", - ActionName = ActionName, - ControllerName = ControllerName, - AttributeRouteInfo = new AttributeRouteInfo - { - Name = RouteName, - }, - }; - - _httpContext = Substitute.For(); - - _actionExecutingContext = new ActionExecutingContext( - new ActionContext(_httpContext, new RouteData(), _controllerActionDescriptor), - new List(), - new Dictionary(), - FilterTestsHelper.CreateMockRetrieveController()); - - var routeValueDictionary = new RouteValueDictionary - { - { KnownActionParameterNames.StudyInstanceUid, "123" }, - { KnownActionParameterNames.PartitionName, Partition.DefaultName }, - }; - _actionExecutingContext.RouteData = new RouteData(routeValueDictionary); - - _nextActionDelegate = Substitute.For(); - - _dicomRequestContextAccessor = Substitute.For(); - - _mediator = Substitute.For(); - _mediator.Send(Arg.Any()) - .Returns(new GetOrAddPartitionResponse(Partition.Default)); - - _mediator.Send(Arg.Any()) - .Returns(new GetPartitionResponse(Partition.Default)); - - _featureConfiguration = Options.Create(new FeatureConfiguration { EnableDataPartitions = true }); - - _filterAttribute = new PopulateDataPartitionFilterAttribute(_dicomRequestContextAccessor, _mediator, _featureConfiguration); - } - - [Fact] - public Task GivenRetrieveRequestWithDataPartitionsEnabled_WhenNoPartitionId_ThenItShouldThrowError() - { - var routeValueDictionary = new RouteValueDictionary - { - { KnownActionParameterNames.StudyInstanceUid, "123" }, - }; - _actionExecutingContext.RouteData = new RouteData(routeValueDictionary); - - return Assert.ThrowsAsync(() => _filterAttribute.OnActionExecutionAsync(_actionExecutingContext, _nextActionDelegate)); - } - - [Fact] - public Task GivenRetrieveRequestWithDataPartitionsDisabled_WhenPartitionIdIsPassed_ThenItShouldThrowError() - { - var routeValueDictionary = new RouteValueDictionary - { - { KnownActionParameterNames.StudyInstanceUid, "123" }, - { KnownActionParameterNames.PartitionName, "partition1" }, - }; - _actionExecutingContext.RouteData = new RouteData(routeValueDictionary); - - _featureConfiguration.Value.EnableDataPartitions = false; - _filterAttribute = new PopulateDataPartitionFilterAttribute(_dicomRequestContextAccessor, _mediator, _featureConfiguration); - - return Assert.ThrowsAsync(() => _filterAttribute.OnActionExecutionAsync(_actionExecutingContext, _nextActionDelegate)); - } - - [Fact] - public async Task GivenRetrieveRequestWithDataPartitionsDisabled_WhenNoPartitionId_ThenItExecutesSuccessfully() - { - var routeValueDictionary = new RouteValueDictionary - { - { KnownActionParameterNames.StudyInstanceUid, "123" }, - }; - _actionExecutingContext.RouteData = new RouteData(routeValueDictionary); - - _featureConfiguration.Value.EnableDataPartitions = false; - _filterAttribute = new PopulateDataPartitionFilterAttribute(_dicomRequestContextAccessor, _mediator, _featureConfiguration); - - await _filterAttribute.OnActionExecutionAsync(_actionExecutingContext, _nextActionDelegate); - } - - [Fact] - public async Task GivenExistingPartitionNamePassed_ThenContextShouldBeSet() - { - await _filterAttribute.OnActionExecutionAsync(_actionExecutingContext, Substitute.For()); - - _dicomRequestContextAccessor.Received().RequestContext.DataPartition = Partition.Default; - } - - [Fact] - public async Task GivenNonExistingPartitionNamePassed_AndStowRequest_ThenPartitionIsCreated() - { - var newPartitionKey = 3; - var newPartitionName = "partition"; - var newPartition = new Partition(newPartitionKey, newPartitionName); - - _controllerActionDescriptor.AttributeRouteInfo.Name = KnownRouteNames.PartitionStoreInstance; - - _mediator.Send(Arg.Any()) - .Returns(new GetOrAddPartitionResponse(null)); - - await _filterAttribute.OnActionExecutionAsync(_actionExecutingContext, _nextActionDelegate); - - _dicomRequestContextAccessor.Received().RequestContext.DataPartition = newPartition; - } - - [Fact] - public async Task GivenNonExistingPartitionNamePassed_AndAddWorkitemRequest_ThenPartitionIsCreated() - { - var newPartitionKey = 3; - var newPartitionName = "partition"; - var newPartition = new Partition(newPartitionKey, newPartitionName); - - _controllerActionDescriptor.AttributeRouteInfo.Name = KnownRouteNames.PartitionedAddWorkitemInstance; - - _mediator.Send(Arg.Any()) - .Returns(new GetOrAddPartitionResponse(null)); - - await _filterAttribute.OnActionExecutionAsync(_actionExecutingContext, _nextActionDelegate); - - _dicomRequestContextAccessor.Received().RequestContext.DataPartition = newPartition; - } -} diff --git a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Filters/QueryModelStateValidatorAttributeTests.cs b/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Filters/QueryModelStateValidatorAttributeTests.cs deleted file mode 100644 index e5220e2bf0..0000000000 --- a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Filters/QueryModelStateValidatorAttributeTests.cs +++ /dev/null @@ -1,54 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Abstractions; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.Routing; -using Microsoft.Health.Dicom.Api.Features.Filters; -using Microsoft.Health.Dicom.Core.Exceptions; -using Xunit; - -namespace Microsoft.Health.Dicom.Api.UnitTests.Features.Filters; - -public class QueryModelStateValidatorAttributeTests -{ - private readonly QueryModelStateValidatorAttribute _validator; - private readonly ActionExecutingContext _context; - - public QueryModelStateValidatorAttributeTests() - { - _context = CreateContext(); - _validator = new QueryModelStateValidatorAttribute(); - _context.ModelState.AddModelError("frames", "This Error Message Should Not be escaped"); - } - - [Fact] - public void Givenvaliderrormessage_shouldnotbeescaped() - { - var ex = Assert.Throws(() => _validator.OnActionExecuting(_context)); - Assert.Equal("The query parameter 'frames' is invalid. This Error Message Should Not be escaped", ex.Message); - } - - [Fact] - public void Giveninvvaliderrormessage_shouldbeescaped() - { - _context.ModelState.Clear(); - _context.ModelState.AddModelError("frames", "This Shoud be <> escaped"); - var ex = Assert.Throws(() => _validator.OnActionExecuting(_context)); - Assert.Equal("The query parameter 'frames' is invalid. This Shoud be <> escaped", ex.Message); - } - - private static ActionExecutingContext CreateContext() - { - return new ActionExecutingContext( - new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()), - new List(), - new Dictionary(), - null); - } -} diff --git a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/ModelBinders/AggregateCsvModelBinderTests.cs b/src/Microsoft.Health.Dicom.Api.UnitTests/Features/ModelBinders/AggregateCsvModelBinderTests.cs deleted file mode 100644 index 76c0ad837e..0000000000 --- a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/ModelBinders/AggregateCsvModelBinderTests.cs +++ /dev/null @@ -1,50 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.Extensions.Primitives; -using Microsoft.Health.Dicom.Api.Features.ModelBinders; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Api.UnitTests.Features.ModelBinders; - -public class AggregateCsvModelBinderTests -{ - [Fact] - public async Task GivenNoValues_WhenBindingModel_ThenReturnNoValue() - { - ModelBindingContext context = Substitute.For(); - context.ModelName = "Example"; - context.ValueProvider.GetValue(context.ModelName).Returns(new ValueProviderResult(new StringValues())); - - IModelBinder binder = new AggregateCsvModelBinder(); - await binder.BindModelAsync(context); - - Assert.True(context.Result.IsModelSet); - Assert.Empty(context.Result.Model as IEnumerable); - } - - [Theory] - [InlineData("foo", "foo")] - [InlineData("1,2", "1", "2")] - [InlineData(" a , b,c\t", "a", "b", "c")] - public async Task GivenValues_WhenBindingModel_ThenSplitByComma(string input, params string[] expected) - { - ModelBindingContext context = Substitute.For(); - context.ModelName = "Example"; - context.ValueProvider.GetValue(context.ModelName).Returns(new ValueProviderResult(new StringValues(input))); - - IModelBinder binder = new AggregateCsvModelBinder(); - await binder.BindModelAsync(context); - - Assert.True(context.Result.IsModelSet); - Assert.True((context.Result.Model as IEnumerable).SequenceEqual(expected)); - } -} diff --git a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/ModelBinders/IntArrayModelBinderTests.cs b/src/Microsoft.Health.Dicom.Api.UnitTests/Features/ModelBinders/IntArrayModelBinderTests.cs deleted file mode 100644 index f97147f18d..0000000000 --- a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/ModelBinders/IntArrayModelBinderTests.cs +++ /dev/null @@ -1,57 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.Extensions.Primitives; -using Microsoft.Health.Dicom.Api.Features.ModelBinders; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Api.UnitTests.Features.ModelBinders; - -public class IntArrayModelBinderTests -{ - [Theory] - [InlineData("", new int[0])] - [InlineData(null, new int[0])] - [InlineData("1, -234, 34", new int[] { 1, -234, 34 })] - public async Task GivenStringContent_WhenBindingIntArrayData_ModelIsSetAndExpectedResultIsParsed(string contextValue, int[] expectedResult) - { - EnsureArg.IsNotNull(expectedResult, nameof(expectedResult)); - ModelBindingContext bindingContext = Substitute.For(); - bindingContext.ValueProvider.GetValue(bindingContext.ModelName).Returns(new ValueProviderResult(new StringValues(contextValue))); - - IModelBinder modelBinder = new IntArrayModelBinder(); - await modelBinder.BindModelAsync(bindingContext); - - Assert.True(bindingContext.Result.IsModelSet); - - var actualResult = bindingContext.Result.Model as int[]; - Assert.Equal(expectedResult.Length, actualResult.Length); - - for (var i = 0; i < expectedResult.Length; i++) - { - Assert.Equal(expectedResult[i], actualResult[i]); - } - } - - [Theory] - [InlineData("1, 2, helloworld")] - [InlineData("1, #5$, 3")] - public async Task GivenInvalidStringContent_WhenBindingIntArrayData_ModelIsNotSet(string contextValue) - { - ModelBindingContext bindingContext = Substitute.For(); - bindingContext.ModelName = "foo"; - bindingContext.ModelState = new ModelStateDictionary(); - bindingContext.ValueProvider.GetValue(bindingContext.ModelName).Returns(new ValueProviderResult(new StringValues(contextValue))); - - IModelBinder modelBinder = new IntArrayModelBinder(); - await modelBinder.BindModelAsync(bindingContext); - - Assert.Equal(1, bindingContext.ModelState.ErrorCount); - } -} diff --git a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/ModelBinders/MandatoryTimeZoneBinderTests.cs b/src/Microsoft.Health.Dicom.Api.UnitTests/Features/ModelBinders/MandatoryTimeZoneBinderTests.cs deleted file mode 100644 index 82624bfdcb..0000000000 --- a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/ModelBinders/MandatoryTimeZoneBinderTests.cs +++ /dev/null @@ -1,110 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.Extensions.Primitives; -using Microsoft.Health.Dicom.Api.Features.ModelBinders; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Api.UnitTests.Models.Binding; - -public class MandatoryTimeZoneBinderTests -{ - private const string ModelName = "Timestamp"; - - private readonly MandatoryTimeZoneBinder _binder = new MandatoryTimeZoneBinder(); - private readonly DefaultModelBindingContext _bindingContext = new DefaultModelBindingContext(); - private readonly IValueProvider _valueProvider = Substitute.For(); - - public static readonly IEnumerable EmptyInputs = new object[][] - { - new object[] { new ValueProviderResult(new StringValues((string)null)) }, - new object[] { ValueProviderResult.None }, - }; - - public MandatoryTimeZoneBinderTests() - { - _bindingContext.ModelState = new ModelStateDictionary(); - _bindingContext.ValueProvider = _valueProvider; - } - - [Theory] - [MemberData(nameof(EmptyInputs))] - public async Task GivenNoInput_WhenBinding_ThenSkip(ValueProviderResult result) - { - _bindingContext.ModelName = ModelName; - _valueProvider.GetValue(ModelName).Returns(result); - - await _binder.BindModelAsync(_bindingContext); - Assert.False(_bindingContext.Result.IsModelSet); - Assert.True(_bindingContext.ModelState.IsValid); - Assert.Null(_bindingContext.Result.Model); - - _valueProvider.Received(1).GetValue(ModelName); - } - - [Theory] - [InlineData("")] - [InlineData(" ")] - [InlineData("\t")] - public async Task GivenBlankInput_WhenBinding_ThenSkip(string input) - { - _bindingContext.ModelName = ModelName; - _valueProvider.GetValue(ModelName).Returns(new ValueProviderResult(new StringValues(input))); - - await _binder.BindModelAsync(_bindingContext); - Assert.False(_bindingContext.Result.IsModelSet); - Assert.False(_bindingContext.ModelState.IsValid); - Assert.Null(_bindingContext.Result.Model); - - _valueProvider.Received(1).GetValue(ModelName); - } - - [Theory] - [InlineData("2023-04-26T11:23:40.9025193-07:00")] - [InlineData("2023-04-26T11:23:40.902519-07:0")] - [InlineData("2023-04-26T11:23:40.90251-07")] - [InlineData("2023-04-26T11:23:40.9025+7:00")] - [InlineData("2023-04-26T11:23:40.902+7:0")] - [InlineData("2023-04-26T11:23:40.90+7")] - [InlineData("2023-04-26T11:23:40.9Z")] - [InlineData("Wed, 26 Apr 2023 18:23:40 GMT")] - public async Task GivenValidString_WhenBinding_ThenSucceed(string input) - { - _bindingContext.ModelName = ModelName; - _valueProvider.GetValue(ModelName).Returns(new ValueProviderResult(new StringValues(input))); - - await _binder.BindModelAsync(_bindingContext); - Assert.True(_bindingContext.Result.IsModelSet); - Assert.Equal(0, _bindingContext.ModelState.ErrorCount); - Assert.Equal(DateTimeOffset.Parse(input, CultureInfo.InvariantCulture), _bindingContext.Result.Model); - - _valueProvider.Received(1).GetValue(ModelName); - } - - [Theory] - [InlineData("foo")] - [InlineData("4/26/2023 5:38:06 PM")] - [InlineData("2023-04-26T11:23:40.9025193")] - [InlineData("2023-04-26T11:23:40X")] - [InlineData("2023-04-26T11:23:40+101:00")] - [InlineData("2023-04-26")] - public async Task GivenInvalidString_WhenBinding_ThenFail(string input) - { - _bindingContext.ModelName = ModelName; - _valueProvider.GetValue(ModelName).Returns(new ValueProviderResult(new StringValues(input))); - - await _binder.BindModelAsync(_bindingContext); - Assert.False(_bindingContext.Result.IsModelSet); - Assert.Equal(1, _bindingContext.ModelState.ErrorCount); - - _valueProvider.Received(1).GetValue(ModelName); - } -} diff --git a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/ModelBinders/TransferSyntaxModelBinderTests.cs b/src/Microsoft.Health.Dicom.Api.UnitTests/Features/ModelBinders/TransferSyntaxModelBinderTests.cs deleted file mode 100644 index 56722dbae4..0000000000 --- a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/ModelBinders/TransferSyntaxModelBinderTests.cs +++ /dev/null @@ -1,40 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.Extensions.Primitives; -using Microsoft.Health.Dicom.Api.Features.ModelBinders; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Api.UnitTests.Features.ModelBinders; - -public class TransferSyntaxModelBinderTests -{ - private const string TransferSyntaxHeaderPrefix = "transfer-syntax"; - - [Theory] - [InlineData("application/dicom;traNSFer-sYNTAx=*", "*")] - [InlineData("application/dicom;traNSFer-sYNTAx=\"*\"", "*")] - [InlineData("application/dicom;traNSFer-sYNTAx=\"LittleEndian\"", "LittleEndian")] - public async Task GivenHeaderWithValidTransferSyntax_WhenBindingTransferSyntax_ModelIsSetAndExpectedResultIsParsed(string contextValue, string expectedResult) - { - ModelBindingContext bindingContext = Substitute.For(); - bindingContext.HttpContext.Request.Headers.Accept.Returns(new StringValues(contextValue)); - - ModelStateDictionary modelStateDictionary = new ModelStateDictionary(); - bindingContext.ModelState.Returns(modelStateDictionary); - - IModelBinder modelBinder = new TransferSyntaxModelBinder(); - await modelBinder.BindModelAsync(bindingContext); - - Assert.True(bindingContext.Result.IsModelSet); - - var actualResult = bindingContext.Result.Model as string; - Assert.Equal(expectedResult, actualResult); - Assert.Equal(expectedResult, bindingContext.ModelState[TransferSyntaxHeaderPrefix].RawValue); - } -} diff --git a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Routing/UrlResolverTests.cs b/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Routing/UrlResolverTests.cs deleted file mode 100644 index a5bb5a019a..0000000000 --- a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Routing/UrlResolverTests.cs +++ /dev/null @@ -1,176 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.AspNetCore.Mvc.Routing; -using Microsoft.AspNetCore.Routing; -using Microsoft.Health.Dicom.Api.Features.Routing; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Operations; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Api.UnitTests.Features.Routing; - -public class UrlResolverTests -{ - private const string DefaultScheme = "http"; - private const string DefaultHost = "test"; - - private readonly IUrlHelperFactory _urlHelperFactory = Substitute.For(); - private readonly IHttpContextAccessor _httpContextAccessor = Substitute.For(); - private readonly IActionContextAccessor _actionContextAccessor = Substitute.For(); - - private readonly UrlResolver _urlResolver; - - private readonly IUrlHelper _urlHelper = Substitute.For(); - private readonly DefaultHttpContext _httpContext = new DefaultHttpContext(); - private readonly ActionContext _actionContext = new ActionContext(); - private readonly LinkGenerator _linkGenerator = Substitute.For(); - - private UrlRouteContext _capturedUrlRouteContext; - - public UrlResolverTests() - { - _httpContext.Request.Scheme = DefaultScheme; - _httpContext.Request.Host = new HostString(DefaultHost); - - _httpContextAccessor.HttpContext.Returns(_httpContext); - - _actionContextAccessor.ActionContext.Returns(_actionContext); - - _urlHelper.RouteUrl( - Arg.Do(c => _capturedUrlRouteContext = c)); - - _urlHelperFactory.GetUrlHelper(_actionContext).Returns(_urlHelper); - - _urlHelper.RouteUrl(Arg.Any()).Returns($"{DefaultScheme}://{DefaultHost}"); - - _urlResolver = new UrlResolver( - _urlHelperFactory, - _httpContextAccessor, - _actionContextAccessor, - _linkGenerator); - } - - [Theory] - [InlineData("v1.0-prerelease")] - [InlineData("v1")] - public void GivenOperationId_WhenRetrieveOperationStatusUriIsResolved_ThenCorrectUrlShouldBeReturned(string version) - { - Guid operationId = Guid.NewGuid(); - - _httpContext.Request.RouteValues.Add(KnownActionParameterNames.Version, version); - - _urlResolver.ResolveOperationStatusUri(operationId); - - ValidateUrlRouteContext( - KnownRouteNames.OperationStatus, - routeValues => Assert.Equal(operationId.ToString(OperationId.FormatSpecifier), routeValues[KnownActionParameterNames.OperationId])); - } - - [Theory] - [InlineData("v1.0-prerelease")] - [InlineData("v1")] - public void GivenAStudy_WhenRetrieveStudyUriIsResolved_ThenCorrectUrlShouldBeReturned(string version) - { - const string studyInstanceUid = "123.123"; - - _httpContext.Request.RouteValues.Add(KnownActionParameterNames.Version, version); - - _urlResolver.ResolveRetrieveStudyUri(studyInstanceUid); - - ValidateUrlRouteContext( - KnownRouteNames.RetrieveStudy, - routeValues => - { - Assert.Equal(studyInstanceUid, routeValues[KnownActionParameterNames.StudyInstanceUid]); - }); - } - - [Theory] - [InlineData("v1.0-prerelease")] - [InlineData("v1")] - public void GivenAStudy_WhenRetrieveStudyUriWithPartitionIdIsResolved_ThenCorrectUrlShouldBeReturned(string version) - { - const string studyInstanceUid = "123.123"; - const string partitionName = "partition1"; - _httpContext.Request.RouteValues.Add(KnownActionParameterNames.PartitionName, partitionName); - _httpContext.Request.RouteValues.Add(KnownActionParameterNames.Version, version); - - _urlResolver.ResolveRetrieveStudyUri(studyInstanceUid); - - ValidateUrlRouteContext( - KnownRouteNames.PartitionRetrieveStudy, - routeValues => - { - Assert.Equal(studyInstanceUid, routeValues[KnownActionParameterNames.StudyInstanceUid]); - Assert.Equal(partitionName, routeValues[KnownActionParameterNames.PartitionName]); - }); - } - - [Theory] - [InlineData("v1.0-prerelease")] - [InlineData("v1")] - public void GivenAnInstance_WhenRetrieveInstanceUriIsResolved_ThenCorrectUrlShouldBeReturned(string version) - { - const string studyInstanceUid = "123.123"; - const string seriesInstanceUid = "456.456"; - const string sopInstanceUid = "789.789"; - - _httpContext.Request.RouteValues.Add(KnownActionParameterNames.Version, version); - - var instance = new InstanceIdentifier(studyInstanceUid, seriesInstanceUid, sopInstanceUid, Partition.Default); - - _urlResolver.ResolveRetrieveInstanceUri(instance, false); - - ValidateUrlRouteContext( - KnownRouteNames.RetrieveInstance, - routeValues => - { - Assert.Equal(studyInstanceUid, routeValues[KnownActionParameterNames.StudyInstanceUid]); - Assert.Equal(seriesInstanceUid, routeValues[KnownActionParameterNames.SeriesInstanceUid]); - Assert.Equal(sopInstanceUid, routeValues[KnownActionParameterNames.SopInstanceUid]); - }); - } - - [Theory] - [InlineData("v1.0-prerelease")] - [InlineData("v1")] - public void GivenAnInstance_WhenResolveRetrieveWorkitemUriResolved_ThenCorrectUrlShouldBeReturned(string version) - { - const string workitemInstanceUid = "123.123"; - const string partitionName = "partition1"; - _httpContext.Request.RouteValues.Add(KnownActionParameterNames.PartitionName, partitionName); - _httpContext.Request.RouteValues.Add(KnownActionParameterNames.Version, version); - - _urlResolver.ResolveRetrieveWorkitemUri(workitemInstanceUid); - - ValidateUrlRouteContext( - KnownRouteNames.PartitionedRetrieveWorkitemInstance, - routeValues => - { - Assert.Equal(workitemInstanceUid, routeValues[KnownActionParameterNames.WorkItemInstanceUid]); - Assert.Equal(partitionName, routeValues[KnownActionParameterNames.PartitionName]); - }); - } - - private void ValidateUrlRouteContext(string routeName, Action routeValuesValidator = null) - { - Assert.NotNull(_capturedUrlRouteContext); - - Assert.Equal(routeName, _capturedUrlRouteContext.RouteName); - Assert.Equal(DefaultScheme, _capturedUrlRouteContext.Protocol); - Assert.Equal(DefaultHost, _capturedUrlRouteContext.Host); - - RouteValueDictionary routeValues = Assert.IsType(_capturedUrlRouteContext.Values); - - routeValuesValidator(routeValues); - } -} diff --git a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Security/QueryStringValidatorMiddlewareTests.cs b/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Security/QueryStringValidatorMiddlewareTests.cs deleted file mode 100644 index 82f6424de5..0000000000 --- a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Security/QueryStringValidatorMiddlewareTests.cs +++ /dev/null @@ -1,63 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.Health.Dicom.Api.Features.Security; -using Microsoft.Health.Dicom.Core.Exceptions; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Api.UnitTests.Features.Security; - -public class QueryStringValidatorMiddlewareTests -{ - private readonly DefaultHttpContext _context; - - public QueryStringValidatorMiddlewareTests() - { - _context = new DefaultHttpContext(); - } - - [Theory] - [InlineData("?")] - [InlineData("?%3Cscript%3Ealert(%27test%27);%3C/script%3E")] - [InlineData("?%3cscript%3ealert(%27test%27);%3c/script%3e")] - public async Task WhenExecutingQueryStringValidatorMiddlewareMiddleware_GivenAnInvalidQueryString_TheExceptionShouldBeThrown(string queryString) - { - QueryStringValidatorMiddleware queryStringValidatorMiddleware = CreateQueryStringValidatorMiddleware(innerHttpContext => Task.CompletedTask); - - _context.Request.QueryString = new QueryString(queryString); - await Assert.ThrowsAsync(() => queryStringValidatorMiddleware.Invoke(_context)); - } - - [Fact] - public async Task WhenExecutingQueryStringValidatorMiddlewareMiddleware_GivenAValidQueryString_TheNoExceptionShouldBeThrown() - { - QueryStringValidatorMiddleware queryStringValidatorMiddleware = CreateQueryStringValidatorMiddleware(innerHttpContext => Task.CompletedTask); - - _context.Request.QueryString = new QueryString("?key=value"); - await queryStringValidatorMiddleware.Invoke(_context); - - Assert.Equal(200, _context.Response.StatusCode); - Assert.Null(_context.Response.ContentType); - } - - [Fact] - public async Task WhenExecutingQueryStringValidatorMiddlewareMiddleware_GivenAnEmptyQueryString_TheNoExceptionShouldBeThrown() - { - QueryStringValidatorMiddleware queryStringValidatorMiddleware = CreateQueryStringValidatorMiddleware(innerHttpContext => Task.CompletedTask); - - await queryStringValidatorMiddleware.Invoke(_context); - - Assert.Equal(200, _context.Response.StatusCode); - Assert.Null(_context.Response.ContentType); - } - - private static QueryStringValidatorMiddleware CreateQueryStringValidatorMiddleware(RequestDelegate nextDelegate) - { - return Substitute.ForPartsOf(nextDelegate); - } -} diff --git a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Security/SecurityModuleTests.cs b/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Security/SecurityModuleTests.cs deleted file mode 100644 index b9fbdaeb34..0000000000 --- a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Security/SecurityModuleTests.cs +++ /dev/null @@ -1,88 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Microsoft.Health.Dicom.Api.Configs; -using Microsoft.Health.Dicom.Api.Modules; -using Microsoft.Health.Dicom.Core.Configs; -using Xunit; - -namespace Microsoft.Health.Dicom.Api.UnitTests.Features.Security; - -public class SecurityModuleTests -{ - [Fact] - public void GivenASecurityConfigurationWithAudience_WhenGettingValidAudiences_ThenCorrectAudienceShouldBeReturned() - { - var dicomServerConfiguration = new DicomServerConfiguration - { - Security = - { - Authentication = new AuthenticationConfiguration - { - Audience = "initialAudience", - }, - }, - }; - - var securityModule = new SecurityModule(dicomServerConfiguration); - - Assert.Equal(new[] { "initialAudience" }, securityModule.GetValidAudiences()); - } - - [Fact] - public void GivenASecurityConfigurationWithAudienceAndAudiences_WhenGettingValidAudiences_ThenCorrectAudienceShouldBeReturned() - { - var dicomServerConfiguration = new DicomServerConfiguration - { - Security = - { - Authentication = new AuthenticationConfiguration - { - Audience = "initialAudience", - Audiences = new[] { "audience1", "audience2" }, - }, - }, - }; - - var securityModule = new SecurityModule(dicomServerConfiguration); - - Assert.Equal(new[] { "audience1", "audience2" }, securityModule.GetValidAudiences()); - } - - [Fact] - public void GivenASecurityConfigurationWithAudiences_WhenGettingValidAudiences_ThenCorrectAudienceShouldBeReturned() - { - var dicomServerConfiguration = new DicomServerConfiguration - { - Security = - { - Authentication = new AuthenticationConfiguration - { - Audiences = new[] { "audience1", "audience2" }, - }, - }, - }; - - var securityModule = new SecurityModule(dicomServerConfiguration); - - Assert.Equal(new[] { "audience1", "audience2" }, securityModule.GetValidAudiences()); - } - - [Fact] - public void GivenASecurityConfigurationWithNoAudienceSpecified_WhenGettingValidAudiences_ThenNullShouldBeReturned() - { - var dicomServerConfiguration = new DicomServerConfiguration - { - Security = - { - Authentication = new AuthenticationConfiguration(), - }, - }; - - var securityModule = new SecurityModule(dicomServerConfiguration); - - Assert.Null(securityModule.GetValidAudiences()); - } -} diff --git a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Web/LazyMultipartReadOnlyStreamTests.cs b/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Web/LazyMultipartReadOnlyStreamTests.cs deleted file mode 100644 index f2f79c5b89..0000000000 --- a/src/Microsoft.Health.Dicom.Api.UnitTests/Features/Web/LazyMultipartReadOnlyStreamTests.cs +++ /dev/null @@ -1,137 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Health.Dicom.Api.Web; -using Xunit; - -namespace Microsoft.Health.Dicom.Api.UnitTests.Features.Web; - -/// -/// Validates our LazyMultipartReadOnlyStream output with Asp.net frameworks MultipartContent output -/// -public class LazyMultipartReadOnlyStreamTests -{ - private const string MediaType = "application/octet-stream"; - private const string MultipartContentSubType = "related"; - private const string TransferSyntax = "transfer-syntax"; - private const string TestTransferSyntaxUid = "1.2.840.10008.1.2.1"; - private const string ContentType = "Content-Type"; - - [Theory] - [InlineData(1024, 2048)] - [InlineData(2048, 2048)] - [InlineData(4096, 2048)] - public async Task GivenSingleStream_WhenMultiPartRequested_StreamContentIsCorrect(int streamSize, int bufferSize) - { - // arrange - string boundary = Guid.NewGuid().ToString(); - using Stream originalStream = GetRandomStream(streamSize); - using MemoryStream expectedMemoryStream = await GetMultipartContentStreamAsync(new[] { originalStream }, boundary); - - // act - using Stream lazyStream = new LazyMultipartReadOnlyStream( - GetAsyncEnumerable(new[] { originalStream }), - boundary, - bufferSize, - CancellationToken.None); - using MemoryStream actualMemoryStream = new MemoryStream(); - await lazyStream.CopyToAsync(actualMemoryStream); - - // assert - Assert.True(ValidateStreamContent(actualMemoryStream, expectedMemoryStream)); - } - - [Theory] - [InlineData(1024, 2048)] - [InlineData(2048, 2048)] - [InlineData(4096, 2048)] - public async Task GivenMultipleStreams_WhenMultiPartRequested_StreamContentIsCorrect(int streamSize, int bufferSize) - { - // arrange - string boundary = Guid.NewGuid().ToString(); - using Stream originalStream1 = GetRandomStream(streamSize); - using Stream originalStream2 = GetRandomStream(streamSize); - using MemoryStream expectedMemoryStream = await GetMultipartContentStreamAsync(new[] { originalStream1, originalStream2 }, boundary); - - // act - using Stream lazyStream = new LazyMultipartReadOnlyStream( - GetAsyncEnumerable(new[] { originalStream1, originalStream2 }), - boundary, - bufferSize, - CancellationToken.None); - using MemoryStream actualMemoryStream = new MemoryStream(); - await lazyStream.CopyToAsync(actualMemoryStream); - - // assert - Assert.True(ValidateStreamContent(actualMemoryStream, expectedMemoryStream)); - } - - private static bool ValidateStreamContent(MemoryStream actualStream, MemoryStream expectedStream) - { - if (actualStream.Length != expectedStream.Length) - { - return false; - } - actualStream.Position = 0; - expectedStream.Position = 0; - - var msArray1 = actualStream.ToArray(); - var msArray2 = expectedStream.ToArray(); - - return msArray1.SequenceEqual(msArray2); - } - - private static async Task GetMultipartContentStreamAsync(Stream[] originalStreams, string boundary) - { - var content = new MultipartContent(MultipartContentSubType, boundary); - var mediaType = new MediaTypeHeaderValue(MediaType); - mediaType.Parameters.Add(new NameValueHeaderValue(TransferSyntax, TestTransferSyntaxUid)); - - foreach (Stream item in originalStreams) - { - var streamContent = new StreamContent(item); - streamContent.Headers.ContentType = mediaType; - content.Add(streamContent); - } - - MemoryStream stream = new MemoryStream(); - await content.CopyToAsync(stream); - return stream; - } - - private static Stream GetRandomStream(long size, Random random = null) - { - random ??= new Random(Environment.TickCount); - var buffer = new byte[size]; - random.NextBytes(buffer); - return new MemoryStream(buffer); - } - - private static async IAsyncEnumerable GetAsyncEnumerable(Stream[] streams) - { - List>> headers = new List>>(); - headers.Add(new KeyValuePair>(ContentType, new List { $"{MediaType}; {TransferSyntax}={TestTransferSyntaxUid}" })); - - await Task.Run(() => 1); - foreach (var stream in streams) - { - stream.Position = 0; - yield return new DicomStreamContent() - { - Stream = stream, - Headers = headers, - StreamLength = stream.Length, - }; - } - } -} diff --git a/src/Microsoft.Health.Dicom.Api.UnitTests/Microsoft.Health.Dicom.Api.UnitTests.csproj b/src/Microsoft.Health.Dicom.Api.UnitTests/Microsoft.Health.Dicom.Api.UnitTests.csproj deleted file mode 100644 index d7d8ec3574..0000000000 --- a/src/Microsoft.Health.Dicom.Api.UnitTests/Microsoft.Health.Dicom.Api.UnitTests.csproj +++ /dev/null @@ -1,32 +0,0 @@ - - - - $(LatestVersion) - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Microsoft.Health.Dicom.Api.UnitTests/Models/UpdateExtendedQueryTagOptionsTests.cs b/src/Microsoft.Health.Dicom.Api.UnitTests/Models/UpdateExtendedQueryTagOptionsTests.cs deleted file mode 100644 index a4d5f07c49..0000000000 --- a/src/Microsoft.Health.Dicom.Api.UnitTests/Models/UpdateExtendedQueryTagOptionsTests.cs +++ /dev/null @@ -1,43 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Linq; -using System.Text.Json; -using Xunit; - -namespace Microsoft.Health.Dicom.Api.Models; - -public class UpdateExtendedQueryTagOptionsTests -{ - [Fact] - public void GivenNoExtensionData_WhenValidate_ShouldReturnEmpty() - { - var options = new UpdateExtendedQueryTagOptions(); - Assert.Empty(options.Validate(null)); - } - - [Fact] - public void GivenExtensionDatas_WhenValidate_ShouldReturnMultipleResults() - { - var options = new UpdateExtendedQueryTagOptions(); - string key1 = "key1"; - string key2 = "key2"; - var data = new Dictionary(); - data.Add(key1, default); - data.Add(key2, default); - options.ExtensionData = data; - var result = options.Validate(null).ToArray(); - Assert.Equal(data.Count, result.Length); - string[] keys = { key1, key2 }; - for (int i = 0; i < result.Length; i++) - { - Assert.Single(result[i].MemberNames); - Assert.Equal(keys[i], result[i].MemberNames.First()); - Assert.Equal($"The field is not supported: \"{keys[i]}\".", result[i].ErrorMessage); - } - } - -} diff --git a/src/Microsoft.Health.Dicom.Api.UnitTests/Web/AspNetCoreMultipartReaderTests.cs b/src/Microsoft.Health.Dicom.Api.UnitTests/Web/AspNetCoreMultipartReaderTests.cs deleted file mode 100644 index b2573521f6..0000000000 --- a/src/Microsoft.Health.Dicom.Api.UnitTests/Web/AspNetCoreMultipartReaderTests.cs +++ /dev/null @@ -1,334 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using Microsoft.Health.Abstractions.Exceptions; -using Microsoft.Health.Dicom.Api.Web; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Web; -using Microsoft.Health.Dicom.WebUtilities; -using NSubstitute; -using NSubstitute.ExceptionExtensions; -using Xunit; -using NotSupportedException = Microsoft.Health.Dicom.Core.Exceptions.NotSupportedException; - -namespace Microsoft.Health.Dicom.Api.UnitTests.Web; - -public class AspNetCoreMultipartReaderTests -{ - private const string DefaultContentType = "multipart/related; boundary=+b+"; - private const string DefaultBodyPartSeparator = "--+b+"; - private const string DefaultBodyPartFinalSeparator = "--+b+--"; - - private readonly ISeekableStreamConverter _seekableStreamConverter; - - public AspNetCoreMultipartReaderTests() - { - _seekableStreamConverter = new HttpSeekableStreamConverter(Substitute.For(), NullLogger.Instance); - } - - [Fact] - public void GivenInvalidContentType_WhenInitializing_ThenUnsupportedMediaTypeExceptionShouldBeThrown() - { - Assert.Throws(() => Create("invalid")); - } - - [Fact] - public void GivenNonMultipartRelatedContentType_WhenInitializing_ThenUnsupportedMediaTypeExceptionShouldBeThrown() - { - Assert.Throws(() => Create("multipart/form-data; boundary=123")); - } - - [Fact] - public void GivenMultipartRelatedContentTypeWithoutBoundary_WhenInitializing_ThenUnsupportedMediaTypeExceptionShouldBeThrown() - { - Assert.Throws(() => Create("multipart/form-data; type=\"application/dicom\"")); - } - - [Fact] - public async Task GivenASingleBodyPartWithContentType_WhenReading_ThenCorrectMultipartBodyPartShouldBeReturned() - { - string body = GenerateBody( - DefaultBodyPartSeparator, - $"Content-Type: application/dicom", - string.Empty, - "content", - DefaultBodyPartFinalSeparator); - - await ExecuteAndValidateAsync( - body, - DefaultContentType, - null, - async bodyPart => await ValidateMultipartBodyPartAsync("application/dicom", "content", bodyPart)); - } - - [Theory] - [InlineData("type=text", "text")] - [InlineData("type=\"text/plain\"", "text/plain")] - public async Task GivenASingleBodyPartWithoutContentTypeAndRequestContentTypeWithTypeParameter_WhenReading_ThenTypeParameterFromRequestContentTypeShouldBeUsed(string typeParameterValue, string expectedTypeValue) - { - string requestContentType = $"multipart/related; {typeParameterValue}; boundary=+b+"; - string body = GenerateBody( - DefaultBodyPartSeparator, - string.Empty, - "content", - DefaultBodyPartFinalSeparator); - - await ExecuteAndValidateAsync( - body, - requestContentType, - null, - async bodyPart => await ValidateMultipartBodyPartAsync(expectedTypeValue, "content", bodyPart)); - } - - [Fact] - public async Task GivenASingleBodyPartWithContentTypeAndRequestContentTypeWithTypeParameter_WhenReading_ThenContentTypeFromBodyPartShouldBeUsed() - { - const string requestContentType = "multipart/related; type=\"text/plain\"; boundary=+b+"; - string body = GenerateBody( - DefaultBodyPartSeparator, - "Content-Type: application/dicom", - string.Empty, - "content", - DefaultBodyPartFinalSeparator); - - await ExecuteAndValidateAsync( - body, - requestContentType, - null, - async bodyPart => await ValidateMultipartBodyPartAsync("application/dicom", "content", bodyPart)); - } - - [Fact] - public async Task GivenMultipeBodyPartsWithoutContentTypeAndRequestContentTypeWithTypeParameter_WhenReading_ThenContentTypeFromBodyPartShouldBeUsedOnlyForFirstBodyPart() - { - const string requestContentType = "multipart/related; type=\"text/plain\"; boundary=+b+"; - string body = GenerateBody( - DefaultBodyPartSeparator, - string.Empty, - "content", - DefaultBodyPartSeparator, - string.Empty, - "content2", - DefaultBodyPartFinalSeparator); - - await ExecuteAndValidateAsync( - body, - requestContentType, - null, - async bodyPart => await ValidateMultipartBodyPartAsync("text/plain", "content", bodyPart), - async bodyPart => await ValidateMultipartBodyPartAsync(null, "content2", bodyPart)); - } - - [Fact] - public async Task GivenMultipeBodyParts_WhenReading_ThenCorrectMultipartBodyPartShouldBeReturned() - { - const string requestContentType = "multipart/related; type=\"text/plain\"; boundary=+123+"; - string body = GenerateBody( - "--+123+", - string.Empty, - "content", - "--+123+", - "Content-Type: application/dicom+json", - string.Empty, - "content2", - "--+123+--"); - - await ExecuteAndValidateAsync( - body, - requestContentType, - null, - async bodyPart => await ValidateMultipartBodyPartAsync("text/plain", "content", bodyPart), - async bodyPart => await ValidateMultipartBodyPartAsync("application/dicom+json", "content2", bodyPart)); - } - - [Fact] - public async Task GivenMissingMultipartBodyPartException_WhenReading_NoMoreSectionShouldBeReturned() - { - string body = GenerateBody( - DefaultBodyPartSeparator, - "Content-Type: application/dicom", - string.Empty, - "content", - DefaultBodyPartSeparator, - "Content-Type: application/dicom+json", - string.Empty, - "content2", - DefaultBodyPartFinalSeparator); - - ISeekableStreamConverter seekableStreamConverter = Substitute.For(); - - seekableStreamConverter.ConvertAsync(default, default).ThrowsForAnyArgs(new IOException()); - - using (MemoryStream stream = await CreateMemoryStream(body)) - { - AspNetCoreMultipartReader aspNetCoreMultipartReader = Create(DefaultContentType, stream, seekableStreamConverter); - - MultipartBodyPart result = await aspNetCoreMultipartReader.ReadNextBodyPartAsync(cancellationToken: default); - - Assert.Null(result); - } - } - - [Fact] - public void GivenStartParameter_WhenReading_ThenDicomNotSupportedExceptionShouldBeThrown() - { - const string requestContentType = "multipart/related; type=\"application/dicom\"; start=\"somewhere\"; boundary=+b+"; - - Assert.Throws(() => Create(requestContentType)); - } - - [Fact] - public async Task GivenAnIOExceptionReadingStream_WhenConverted_ShouldReturnNull() - { - ISeekableStreamConverter seekableStreamConverter = Substitute.For(); - - string body = GenerateBody( - DefaultBodyPartSeparator, - $"Content-Type: application/dicom", - string.Empty, - "content", - DefaultBodyPartFinalSeparator); - - seekableStreamConverter.ConvertAsync(Arg.Any(), Arg.Any()).Throws(new IOException()); - - await ExecuteAndValidateAsync( - body, - DefaultContentType, - seekableStreamConverter, - bodyPart => { Assert.Null(bodyPart); return Task.CompletedTask; }); - } - - [Fact] - public async Task GivenAInvalidDataException__ThenPayloadTooLargeExceptionShouldBeRethrown() - { - ISeekableStreamConverter seekableStreamConverter = Substitute.For(); - - string body = GenerateBody( - DefaultBodyPartSeparator, - $"Content-Type: application/dicom", - string.Empty, - "content", - DefaultBodyPartFinalSeparator); - - - seekableStreamConverter.ConvertAsync(Arg.Any(), Arg.Any()).Throws(new InvalidDataException()); - - await Assert.ThrowsAsync( - () => ExecuteAndValidateAsync( - body, - DefaultContentType, - seekableStreamConverter, - async bodyPart => await ValidateMultipartBodyPartAsync("application/dicom", "content", bodyPart))); - } - - [Fact] - public async Task GivenStreamMissingEndingBoundary_WhenReading_ThenInvalidMultipartRequestExceptionShouldBeThrown() - { - ISeekableStreamConverter seekableStreamConverter = Substitute.For(); - seekableStreamConverter.ConvertAsync(Arg.Any(), Arg.Any()).Returns(Stream.Null); - - string invalidBody = GenerateBody( - DefaultBodyPartSeparator, - $"Content-Type: application/dicom", - string.Empty, - "content"); - - await Assert.ThrowsAsync( - () => ExecuteAndValidateAsync( - invalidBody, - DefaultContentType, - seekableStreamConverter, - bodyPart => Task.CompletedTask)); - } - - private AspNetCoreMultipartReader Create(string contentType, Stream body = null, ISeekableStreamConverter seekableStreamConverter = null) - { - if (body == null) - { - body = new MemoryStream(); - } - - if (seekableStreamConverter == null) - { - seekableStreamConverter = _seekableStreamConverter; - } - - return new AspNetCoreMultipartReader( - contentType, - body, - seekableStreamConverter, - CreateStoreConfiguration()); - } - - private static IOptions CreateStoreConfiguration() - { - var configuration = Substitute.For>(); - configuration.Value.Returns(new StoreConfiguration - { - MaxAllowedDicomFileSize = 1000000, - }); - return configuration; - } - - private static async Task CreateMemoryStream(string content) - { - MemoryStream stream = new MemoryStream(); - - using (StreamWriter writer = new StreamWriter(stream, leaveOpen: true)) - { - await writer.WriteAsync(content); - } - - stream.Seek(0, SeekOrigin.Begin); - - return stream; - } - - private async Task ExecuteAndValidateAsync(string body, string requestContentType, ISeekableStreamConverter seekableStreamConverter, params Func[] validators) - { - using (MemoryStream stream = await CreateMemoryStream(body)) - { - AspNetCoreMultipartReader aspNetCoreMultipartReader = Create(requestContentType, stream, seekableStreamConverter); - - MultipartBodyPart result = null; - - foreach (Func validator in validators) - { - result = await aspNetCoreMultipartReader.ReadNextBodyPartAsync(cancellationToken: default); - - await validator(result); - } - - result = await aspNetCoreMultipartReader.ReadNextBodyPartAsync(cancellationToken: default); - - Assert.Null(result); - } - } - - private static async Task ValidateMultipartBodyPartAsync(string expectedContentType, string expectedBody, MultipartBodyPart actual) - { - Assert.NotNull(actual); - Assert.Equal(expectedContentType, actual.ContentType); - - using (StreamReader reader = new StreamReader(actual.SeekableStream)) - { - Assert.Equal(expectedBody, await reader.ReadToEndAsync()); - } - } - - private static string GenerateBody(params string[] lines) - { - // Body part requires \r\n as separator per RFC2616. - return string.Join("\r\n", lines); - } -} diff --git a/src/Microsoft.Health.Dicom.Api.UnitTests/Web/HttpSeekableStreamConverterTests.cs b/src/Microsoft.Health.Dicom.Api.UnitTests/Web/HttpSeekableStreamConverterTests.cs deleted file mode 100644 index 3395e766f4..0000000000 --- a/src/Microsoft.Health.Dicom.Api.UnitTests/Web/HttpSeekableStreamConverterTests.cs +++ /dev/null @@ -1,77 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Api.Web; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.WebUtilities; -using NSubstitute; -using NSubstitute.ExceptionExtensions; -using Xunit; - -namespace Microsoft.Health.Dicom.Api.UnitTests.Web; - -public class HttpSeekableStreamConverterTests -{ - private readonly HttpSeekableStreamConverter _seekableStreamConverter; - - public HttpSeekableStreamConverterTests() - { - IOptions configuration = Substitute.For>(); - configuration.Value.Returns(new StoreConfiguration - { - MaxAllowedDicomFileSize = 1000000, - }); - _seekableStreamConverter = new HttpSeekableStreamConverter(Substitute.For(), NullLogger.Instance); - } - - [Fact] - public async Task GivenANonSeekableStream_WhenConverted_ThenANewSeekableStreamShouldBeReturned() - { - Stream nonseekableStream = Substitute.For(); - - nonseekableStream.CanSeek.Returns(false); - - Stream seekableStream = await _seekableStreamConverter.ConvertAsync(nonseekableStream, CancellationToken.None); - - Assert.NotNull(seekableStream); - Assert.True(seekableStream.CanSeek); - } - - [Fact] - public async Task GivenAnIOExceptionReadingStream_WhenConverted_ThenIOExceptionShouldBeRethrown() - { - Stream nonseekableStream = SetupNonSeekableStreamException(); - - await Assert.ThrowsAsync( - () => _seekableStreamConverter.ConvertAsync(nonseekableStream, CancellationToken.None)); - } - - [Fact] - public async Task GivenANoneIOExceptionReadingStream_WhenConverted_ThenExceptionShouldBeRethrown() - { - Stream nonseekableStream = SetupNonSeekableStreamException(); - - await Assert.ThrowsAsync( - () => _seekableStreamConverter.ConvertAsync(nonseekableStream, CancellationToken.None)); - } - - private static Stream SetupNonSeekableStreamException() - where TException : Exception, new() - { - Stream nonseekableStream = Substitute.For(); - - nonseekableStream.CanSeek.Returns(false); - nonseekableStream.ReadAsync(Arg.Any>(), Arg.Any()).ThrowsForAnyArgs(); - - return nonseekableStream; - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Configs/DicomServerConfiguration.cs b/src/Microsoft.Health.Dicom.Api/Configs/DicomServerConfiguration.cs deleted file mode 100644 index c310b4b3fe..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Configs/DicomServerConfiguration.cs +++ /dev/null @@ -1,26 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Microsoft.Health.Api.Configuration; -using Microsoft.Health.Api.Features.Cors; -using Microsoft.Health.Core.Configs; -using Microsoft.Health.Dicom.Core.Configs; - -namespace Microsoft.Health.Dicom.Api.Configs; - -public class DicomServerConfiguration : IApiConfiguration -{ - public FeatureConfiguration Features { get; } = new FeatureConfiguration(); - - public SecurityConfiguration Security { get; } = new SecurityConfiguration(); - - public CorsConfiguration Cors { get; } = new CorsConfiguration(); - - public ServicesConfiguration Services { get; } = new ServicesConfiguration(); - - public AuditConfiguration Audit { get; } = new AuditConfiguration("X-MS-AZUREDICOM-AUDIT-"); - - public SwaggerConfiguration Swagger { get; } = new SwaggerConfiguration(); -} diff --git a/src/Microsoft.Health.Dicom.Api/Configs/SwaggerConfiguration.cs b/src/Microsoft.Health.Dicom.Api/Configs/SwaggerConfiguration.cs deleted file mode 100644 index ee25d391ec..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Configs/SwaggerConfiguration.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using Microsoft.OpenApi.Models; - -namespace Microsoft.Health.Dicom.Api.Configs; - -public class SwaggerConfiguration -{ - public Uri ServerUri { get; set; } - - public string Title { get; set; } = "Medical Imaging Server for DICOM"; - - public OpenApiLicense License { get; } = new() - { - Name = "MIT License", - Url = new System.Uri("https://github.com/microsoft/dicom-server/blob/main/LICENSE"), - }; -} diff --git a/src/Microsoft.Health.Dicom.Api/Controllers/ChangeFeedController.cs b/src/Microsoft.Health.Dicom.Api/Controllers/ChangeFeedController.cs deleted file mode 100644 index 2a76f4c027..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Controllers/ChangeFeedController.cs +++ /dev/null @@ -1,130 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using MediatR; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Microsoft.Health.Api.Features.Audit; -using Microsoft.Health.Dicom.Api.Extensions; -using Microsoft.Health.Dicom.Api.Features.Filters; -using Microsoft.Health.Dicom.Api.Features.Routing; -using Microsoft.Health.Dicom.Api.Models; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Audit; -using Microsoft.Health.Dicom.Core.Features.ChangeFeed; -using Microsoft.Health.Dicom.Core.Messages.ChangeFeed; -using Microsoft.Health.Dicom.Core.Models; -using Microsoft.Health.Dicom.Core.Web; -using DicomAudit = Microsoft.Health.Dicom.Api.Features.Audit; - -namespace Microsoft.Health.Dicom.Api.Controllers; - -[QueryModelStateValidator] -[ServiceFilter(typeof(DicomAudit.AuditLoggingFilterAttribute))] -public class ChangeFeedController : ControllerBase -{ - /* - * The current offset/limit pattern used in DICOMweb and the other paginated APIs may have performance - * issues as the size of the offset increases. That is because while SQL Server can use the index to - * seek to a particular value easily, it still needs to reads the number of rows in the offset to figure - * out where to begin returning rows. So if the offset is 1000 and the limit is 5, SQL will read 1005 rows. - */ - - private readonly IMediator _mediator; - private readonly ILogger _logger; - - public ChangeFeedController(IMediator mediator, ILogger logger) - { - _mediator = EnsureArg.IsNotNull(mediator, nameof(mediator)); - _logger = EnsureArg.IsNotNull(logger, nameof(logger)); - } - - [HttpGet] - [MapToApiVersion("1.0-prerelease")] - [MapToApiVersion("1.0")] - [Produces(KnownContentTypes.ApplicationJson)] - [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] - [ProducesResponseType((int)HttpStatusCode.Unauthorized)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)] - [VersionedRoute(KnownRoutes.ChangeFeed)] - [AuditEventType(AuditEventSubType.ChangeFeed)] - public Task GetChangeFeedAsync( - [FromQuery][Range(0, long.MaxValue)] long offset = 0, - [FromQuery][Range(1, 100)] int limit = 10, - [FromQuery] bool includeMetadata = true) - { - return GetChangeFeedAsync(TimeRange.MaxValue, offset, limit, ChangeFeedOrder.Sequence, includeMetadata, HttpContext.RequestAborted); - } - - [HttpGet] - [MapToApiVersion("2.0")] - [Produces(KnownContentTypes.ApplicationJson)] - [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] - [ProducesResponseType((int)HttpStatusCode.Unauthorized)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)] - [VersionedRoute(KnownRoutes.ChangeFeed)] - [AuditEventType(AuditEventSubType.ChangeFeed)] - public Task GetChangeFeedAsync( - [FromQuery] WindowedPaginationOptions options, - [FromQuery] bool includeMetadata = true) - { - EnsureArg.IsNotNull(options, nameof(options)); - return GetChangeFeedAsync(options.Window, options.Offset, options.Limit, ChangeFeedOrder.Time, includeMetadata, HttpContext.RequestAborted); - } - - [HttpGet] - [Produces(KnownContentTypes.ApplicationJson)] - [ProducesResponseType(typeof(ChangeFeedEntry), (int)HttpStatusCode.OK)] - [ProducesResponseType((int)HttpStatusCode.Unauthorized)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)] - [VersionedRoute(KnownRoutes.ChangeFeedLatest)] - [AuditEventType(AuditEventSubType.ChangeFeed)] - public async Task GetChangeFeedLatestAsync([FromQuery] bool includeMetadata = true) - { - _logger.LogInformation( - "Received request to read the latest change feed and metadata is {MetadataStatus}.", - includeMetadata ? "included" : "not included"); - - ChangeFeedLatestResponse response = await _mediator.GetChangeFeedLatest( - HttpContext.GetMajorRequestedApiVersion() > 1 ? ChangeFeedOrder.Time : ChangeFeedOrder.Sequence, - includeMetadata, - cancellationToken: HttpContext.RequestAborted); - - return StatusCode((int)HttpStatusCode.OK, response.Entry); - } - - private async Task GetChangeFeedAsync( - TimeRange range, - long offset, - int limit, - ChangeFeedOrder order, - bool includeMetadata, - CancellationToken cancellationToken = default) - { - _logger.LogInformation( - "Received request to read change feed for {Window} with an offset of {Offset} and limit of {Limit}. Metadata is {MetadataStatus}.", - range, - offset, - limit, - includeMetadata ? "included" : "not included"); - - ChangeFeedResponse response = await _mediator.GetChangeFeed( - range, - offset, - limit, - order, - includeMetadata, - cancellationToken); - - return StatusCode((int)HttpStatusCode.OK, response.Entries); - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Controllers/DeleteController.cs b/src/Microsoft.Health.Dicom.Api/Controllers/DeleteController.cs deleted file mode 100644 index bc2d71e90b..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Controllers/DeleteController.cs +++ /dev/null @@ -1,89 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Net; -using System.Threading.Tasks; -using EnsureThat; -using MediatR; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Microsoft.Health.Api.Features.Audit; -using Microsoft.Health.Dicom.Api.Features.Filters; -using Microsoft.Health.Dicom.Api.Features.Routing; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Audit; -using Microsoft.Health.Dicom.Core.Messages.Delete; -using DicomAudit = Microsoft.Health.Dicom.Api.Features.Audit; - -namespace Microsoft.Health.Dicom.Api.Controllers; - -[QueryModelStateValidator] -[ServiceFilter(typeof(DicomAudit.AuditLoggingFilterAttribute))] -[ServiceFilter(typeof(PopulateDataPartitionFilterAttribute))] -public class DeleteController : ControllerBase -{ - private readonly IMediator _mediator; - private readonly ILogger _logger; - - public DeleteController(IMediator mediator, ILogger logger) - { - EnsureArg.IsNotNull(mediator, nameof(mediator)); - EnsureArg.IsNotNull(logger, nameof(logger)); - - _mediator = mediator; - _logger = logger; - } - - [HttpDelete] - [ProducesResponseType((int)HttpStatusCode.NoContent)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.NotFound)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)] - [VersionedPartitionRoute(KnownRoutes.StudyRoute)] - [VersionedRoute(KnownRoutes.StudyRoute)] - [AuditEventType(AuditEventSubType.Delete)] - public async Task DeleteStudyAsync(string studyInstanceUid) - { - _logger.LogInformation("DICOM Web Delete Study request received, with study instance UID {StudyInstanceUid}.", studyInstanceUid); - - DeleteResourcesResponse deleteResponse = await _mediator.DeleteDicomStudyAsync( - studyInstanceUid, cancellationToken: HttpContext.RequestAborted); - - return NoContent(); - } - - [HttpDelete] - [ProducesResponseType((int)HttpStatusCode.NoContent)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.NotFound)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)] - [VersionedPartitionRoute(KnownRoutes.SeriesRoute)] - [VersionedRoute(KnownRoutes.SeriesRoute)] - [AuditEventType(AuditEventSubType.Delete)] - public async Task DeleteSeriesAsync(string studyInstanceUid, string seriesInstanceUid) - { - _logger.LogInformation("DICOM Web Delete Series request received, with study instance UID {StudyInstanceUid} and series UID {SeriesInstanceUid}.", studyInstanceUid, seriesInstanceUid); - - DeleteResourcesResponse deleteResponse = await _mediator.DeleteDicomSeriesAsync( - studyInstanceUid, seriesInstanceUid, cancellationToken: HttpContext.RequestAborted); - - return NoContent(); - } - - [HttpDelete] - [ProducesResponseType((int)HttpStatusCode.NoContent)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.NotFound)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)] - [VersionedPartitionRoute(KnownRoutes.InstanceRoute)] - [VersionedRoute(KnownRoutes.InstanceRoute)] - [AuditEventType(AuditEventSubType.Delete)] - public async Task DeleteInstanceAsync(string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid) - { - _logger.LogInformation("DICOM Web Delete Instance request received, with study instance UID {StudyInstanceUid}, series UID {SeriesInstanceUid} and instance UID {SopInstanceUid}.", studyInstanceUid, seriesInstanceUid, sopInstanceUid); - - DeleteResourcesResponse deleteResponse = await _mediator.DeleteDicomInstanceAsync( - studyInstanceUid, seriesInstanceUid, sopInstanceUid, cancellationToken: HttpContext.RequestAborted); - - return NoContent(); - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Controllers/ExportController.cs b/src/Microsoft.Health.Dicom.Api/Controllers/ExportController.cs deleted file mode 100644 index fbb6ef92f6..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Controllers/ExportController.cs +++ /dev/null @@ -1,102 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.ComponentModel.DataAnnotations; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using MediatR; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Health.Api.Features.Audit; -using Microsoft.Health.Dicom.Api.Extensions; -using Microsoft.Health.Dicom.Api.Features.Conventions; -using Microsoft.Health.Dicom.Api.Features.Filters; -using Microsoft.Health.Dicom.Api.Features.Routing; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Audit; -using Microsoft.Health.Dicom.Core.Messages.Export; -using Microsoft.Health.Dicom.Core.Models.Export; -using Microsoft.Health.Dicom.Core.Web; -using Microsoft.Health.Operations; - -namespace Microsoft.Health.Dicom.Api.Controllers; - -/// -/// Represents an API controller for export operations. -/// -[IntroducedInApiVersion(1)] -[ServiceFilter(typeof(Features.Audit.AuditLoggingFilterAttribute))] -[ServiceFilter(typeof(PopulateDataPartitionFilterAttribute))] -public class ExportController : ControllerBase -{ - private readonly IMediator _mediator; - private readonly ILogger _logger; - private readonly bool _enabled; - - /// - /// Initializes a new instance of the class based on the given options. - /// - /// An used to send requests. - /// Options concerning which features are enabled. - /// A diagnostic logger. - /// - /// , , or is . - /// - public ExportController( - IMediator mediator, - IOptions options, - ILogger logger) - { - _mediator = EnsureArg.IsNotNull(mediator, nameof(mediator)); - _logger = EnsureArg.IsNotNull(logger, nameof(logger)); - FeatureConfiguration config = EnsureArg.IsNotNull(options?.Value, nameof(options)); - _enabled = config.EnableExport && !config.DisableOperations; - } - - /// - /// Asynchronously starts the export operation. - /// - /// The specification that details the source and destination for the export. - /// - /// A task that represents the asynchronous export operation. The value of its - /// property contains the . Upon success, the result will contain an - /// detailing the new export operation instance. Otherwise, the status code - /// provides details as to why the request failed. - /// - [HttpPost] - [BodyModelStateValidator] - [Produces(KnownContentTypes.ApplicationJson)] - [Consumes(KnownContentTypes.ApplicationJson)] - [ProducesResponseType(typeof(OperationReference), (int)HttpStatusCode.Accepted)] - [ProducesResponseType((int)HttpStatusCode.BadRequest)] - [VersionedRoute(KnownRoutes.ExportInstancesRoute)] - [VersionedPartitionRoute(KnownRoutes.ExportInstancesRoute)] - [AuditEventType(AuditEventSubType.Export)] - public async Task ExportAsync([Required][FromBody] ExportSpecification specification) - { - EnsureArg.IsNotNull(specification, nameof(specification)); - - return await GetResultIfEnabledAsync( - async (x, token) => - { - _logger.LogInformation("DICOM Web Export request received to export instances from '{Source}' to '{Sink}'.", x.Source.Type, x.Destination.Type); - - ExportResponse response = await _mediator.ExportAsync(x, token); - - Response.AddLocationHeader(response.Operation.Href); - return StatusCode((int)HttpStatusCode.Accepted, response.Operation); - }, - specification); - } - - private async ValueTask GetResultIfEnabledAsync(Func> factoryAsync, T input) - => _enabled ? await factoryAsync(input, HttpContext.RequestAborted) : NotFound(); -} diff --git a/src/Microsoft.Health.Dicom.Api/Controllers/ExtendedQueryTagController.cs b/src/Microsoft.Health.Dicom.Api/Controllers/ExtendedQueryTagController.cs deleted file mode 100644 index b817f6cae4..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Controllers/ExtendedQueryTagController.cs +++ /dev/null @@ -1,201 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Net; -using System.Net.Mime; -using System.Threading.Tasks; -using EnsureThat; -using MediatR; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Health.Api.Features.Audit; -using Microsoft.Health.Dicom.Api.Extensions; -using Microsoft.Health.Dicom.Api.Features.Filters; -using Microsoft.Health.Dicom.Api.Features.Routing; -using Microsoft.Health.Dicom.Api.Models; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Audit; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Messages.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Web; -using DicomAudit = Microsoft.Health.Dicom.Api.Features.Audit; - -namespace Microsoft.Health.Dicom.Api.Controllers; - -[ServiceFilter(typeof(DicomAudit.AuditLoggingFilterAttribute))] -public class ExtendedQueryTagController : ControllerBase -{ - private readonly IMediator _mediator; - private readonly ILogger _logger; - private readonly bool _asyncOperationDisabled; - - public ExtendedQueryTagController(IMediator mediator, ILogger logger, IOptions featureConfiguration) - { - _mediator = EnsureArg.IsNotNull(mediator, nameof(mediator)); - _logger = EnsureArg.IsNotNull(logger, nameof(logger)); - EnsureArg.IsNotNull(featureConfiguration, nameof(featureConfiguration)); - _asyncOperationDisabled = featureConfiguration.Value.DisableOperations; - } - - [HttpPost] - [BodyModelStateValidator] - [Produces(KnownContentTypes.ApplicationJson)] - [Consumes(KnownContentTypes.ApplicationJson)] - [ProducesResponseType(typeof(AddExtendedQueryTagResponse), (int)HttpStatusCode.Accepted)] - [ProducesResponseType((int)HttpStatusCode.BadRequest)] - [VersionedRoute(KnownRoutes.ExtendedQueryTagRoute)] - [AuditEventType(AuditEventSubType.AddExtendedQueryTag)] - public async Task PostAsync([Required][FromBody] IReadOnlyCollection extendedQueryTags) - { - _logger.LogInformation("DICOM Web Add Extended Query Tag request received, with extendedQueryTags {ExtendedQueryTags}.", extendedQueryTags); - - if (_asyncOperationDisabled) - { - throw new DicomAsyncOperationDisabledException(); - } - - try - { - AddExtendedQueryTagResponse response = await _mediator.AddExtendedQueryTagsAsync(extendedQueryTags, HttpContext.RequestAborted); - - Response.AddLocationHeader(response.Operation.Href); - return StatusCode((int)HttpStatusCode.Accepted, response.Operation); - } - catch (ExistingOperationException ere) - { - Response.AddLocationHeader(ere.ExistingOperation.Href); - return new ContentResult - { - Content = ere.Message, - ContentType = MediaTypeNames.Text.Plain, - StatusCode = (int)HttpStatusCode.Conflict, - }; - } - } - - [Produces(KnownContentTypes.ApplicationJson)] - [ProducesResponseType(typeof(DeleteExtendedQueryTagResponse), (int)HttpStatusCode.NoContent)] - [HttpDelete] - [VersionedRoute(KnownRoutes.DeleteExtendedQueryTagRoute)] - [AuditEventType(AuditEventSubType.RemoveExtendedQueryTag)] - public async Task DeleteAsync(string tagPath) - { - _logger.LogInformation("DICOM Web Delete Extended Query Tag request received, with extended query tag path {TagPath}.", tagPath); - - await _mediator.DeleteExtendedQueryTagAsync(tagPath, HttpContext.RequestAborted); - return StatusCode((int)HttpStatusCode.NoContent); - } - - /// - /// Handles requests to get all extended query tags. - /// - /// Options for configuring which tags are returned. - /// - /// Returns Bad Request if given path can't be parsed. Returns Not Found if given path doesn't map to a stored - /// extended query tag or if no extended query tags are stored. Returns OK with a JSON body of all tags in other cases. - /// - [HttpGet] - [Produces(KnownContentTypes.ApplicationJson)] - [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.NotFound)] - [VersionedRoute(KnownRoutes.ExtendedQueryTagRoute)] - [AuditEventType(AuditEventSubType.GetAllExtendedQueryTags)] - [QueryModelStateValidator] - public async Task GetTagsAsync([FromQuery] PaginationOptions options) - { - // TODO: Enforce the above data annotations with ModelState.IsValid or use the [ApiController] attribute - // for automatic error generation. However, we should change all errors across the API surface. - _logger.LogInformation("DICOM Web Get Extended Query Tag request received for all extended query tags"); - - EnsureArg.IsNotNull(options, nameof(options)); - GetExtendedQueryTagsResponse response = await _mediator.GetExtendedQueryTagsAsync( - options.Limit, - options.Offset, - HttpContext.RequestAborted); - - return StatusCode((int)HttpStatusCode.OK, response.ExtendedQueryTags); - } - - /// - /// Handles requests to get individual extended query tags. - /// - /// Path for requested extended query tag. - /// - /// Returns Bad Request if given path can't be parsed. Returns Not Found if given path doesn't map to a stored - /// extended query tag. Returns OK with a JSON body of requested tag in other cases. - /// - [HttpGet] - [Produces(KnownContentTypes.ApplicationJson)] - [ProducesResponseType(typeof(GetExtendedQueryTagEntry), (int)HttpStatusCode.OK)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.NotFound)] - [VersionedRoute(KnownRoutes.GetExtendedQueryTagRoute, Name = KnownRouteNames.GetExtendedQueryTag)] - [AuditEventType(AuditEventSubType.GetExtendedQueryTag)] - public async Task GetTagAsync(string tagPath) - { - _logger.LogInformation("DICOM Web Get Extended Query Tag request received for extended query tag: {TagPath}", tagPath); - - GetExtendedQueryTagResponse response = await _mediator.GetExtendedQueryTagAsync(tagPath, HttpContext.RequestAborted); - return StatusCode((int)HttpStatusCode.OK, response.ExtendedQueryTag); - } - - /// - /// Handles requests to get extended query tag errors. - /// - /// Path for requested extended query tag. - /// Options for configuring which errors are returned. - /// - /// Returns Bad Request if given path can't be parsed. Returns Not Found if given path doesn't map to a stored - /// error. Returns OK with a JSON body of requested tag error in other cases. - /// - [HttpGet] - [Produces(KnownContentTypes.ApplicationJson)] - [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.NotFound)] - [VersionedRoute(KnownRoutes.GetExtendedQueryTagErrorsRoute, Name = KnownRouteNames.GetExtendedQueryTagErrors)] - [AuditEventType(AuditEventSubType.GetExtendedQueryTagErrors)] - [QueryModelStateValidator] - public async Task GetTagErrorsAsync( - [FromRoute] string tagPath, - [FromQuery] PaginationOptions options) - { - _logger.LogInformation("DICOM Web Get Extended Query Tag Errors request received for extended query tag: {TagPath}", tagPath); - - EnsureArg.IsNotNull(options, nameof(options)); - GetExtendedQueryTagErrorsResponse response = await _mediator.GetExtendedQueryTagErrorsAsync( - tagPath, - options.Limit, - options.Offset, - HttpContext.RequestAborted); - - return StatusCode((int)HttpStatusCode.OK, response.ExtendedQueryTagErrors); - } - - [HttpPatch] - [Produces(KnownContentTypes.ApplicationJson)] - [BodyModelStateValidator] - [ProducesResponseType(typeof(GetExtendedQueryTagEntry), (int)HttpStatusCode.OK)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.NotFound)] - [VersionedRoute(KnownRoutes.UpdateExtendedQueryTagQueryStatusRoute)] - [AuditEventType(AuditEventSubType.UpdateExtendedQueryTag)] - public async Task UpdateTagAsync([FromRoute] string tagPath, [FromBody] UpdateExtendedQueryTagOptions newValue) - { - _logger.LogInformation("DICOM Web Update Extended Query Tag Query Status request received for extended query tag {TagPath} and new value {NewValue}", tagPath, $"{nameof(UpdateExtendedQueryTagOptions.QueryStatus)}: '{newValue?.QueryStatus}'"); - - EnsureArg.IsNotNull(newValue, nameof(newValue)); - UpdateExtendedQueryTagResponse response = await _mediator.UpdateExtendedQueryTagAsync(tagPath, newValue.ToEntry(), HttpContext.RequestAborted); - - return StatusCode((int)HttpStatusCode.OK, response.TagEntry); - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Controllers/OperationsController.cs b/src/Microsoft.Health.Dicom.Api/Controllers/OperationsController.cs deleted file mode 100644 index 29a8838866..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Controllers/OperationsController.cs +++ /dev/null @@ -1,129 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Net; -using System.Threading.Tasks; -using EnsureThat; -using MediatR; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Microsoft.Health.Api.Features.Audit; -using Microsoft.Health.Dicom.Api.Extensions; -using Microsoft.Health.Dicom.Api.Features.Routing; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Audit; -using Microsoft.Health.Dicom.Core.Features.Routing; -using Microsoft.Health.Dicom.Core.Messages.Operations; -using Microsoft.Health.Dicom.Core.Models.Operations; -using Microsoft.Health.Operations; -using DicomApiAuditLoggingFilterAttribute = Microsoft.Health.Dicom.Api.Features.Audit.AuditLoggingFilterAttribute; - -namespace Microsoft.Health.Dicom.Api.Controllers; - -/// -/// Represents the REST API controller for interacting with long-running DICOM operations. -/// -[ServiceFilter(typeof(DicomApiAuditLoggingFilterAttribute))] -public class OperationsController : ControllerBase -{ - private readonly IMediator _mediator; - private readonly IUrlResolver _urlResolver; - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the class. - /// - /// A mediator object for passing requests to corresponding handlers. - /// A helper for building URLs within the application. - /// A controller-specific logger. - /// - /// or is . - /// - public OperationsController( - IMediator mediator, - IUrlResolver urlResolver, - ILogger logger) - { - EnsureArg.IsNotNull(mediator, nameof(mediator)); - EnsureArg.IsNotNull(urlResolver, nameof(urlResolver)); - EnsureArg.IsNotNull(logger, nameof(logger)); - - _mediator = mediator; - _urlResolver = urlResolver; - _logger = logger; - } - - /// - /// Gets the state of a DICOM operation based on its ID. - /// - /// - /// If the operation has not yet completed, then its response will include a "Location" header directing - /// clients to the URL where the status can be checked queried. - /// - /// The unique ID for a particular DICOM operation. - /// - /// A task representing the operation. The value of its - /// property contains the state of the operation, if found; - /// otherwise - /// - /// consists of white space characters. - /// is . - /// The connection was aborted. - [HttpGet] - [VersionedRoute(KnownRoutes.OperationInstanceRoute, Name = KnownRouteNames.OperationStatus)] - [ProducesResponseType((int)HttpStatusCode.BadRequest)] - [ProducesResponseType((int)HttpStatusCode.NotFound)] - [ProducesResponseType(typeof(IOperationState), (int)HttpStatusCode.Accepted)] - [ProducesResponseType(typeof(IOperationState), (int)HttpStatusCode.OK)] - [AuditEventType(AuditEventSubType.Operation)] - public async Task GetStateAsync(Guid operationId) - { - _logger.LogInformation("DICOM Web Get Operation Status request received for ID '{OperationId}'", operationId); - - OperationStateResponse response = await _mediator.GetOperationStateAsync(operationId, HttpContext.RequestAborted); - - if (response == null) - { - return NotFound(); - } - - HttpStatusCode statusCode; - IOperationState state = response.OperationState; - if (state.Status == OperationStatus.NotStarted || state.Status == OperationStatus.Running) - { - Response.AddLocationHeader(_urlResolver.ResolveOperationStatusUri(operationId)); - statusCode = HttpStatusCode.Accepted; - } - else - { - statusCode = HttpStatusCode.OK; - } - - return StatusCode((int)statusCode, UpdateOperationState(state)); - } - - private IOperationState UpdateOperationState(IOperationState operationState) - { - int version = HttpContext.GetMajorRequestedApiVersion(); - - if (version > 1 || operationState.Status != OperationStatus.Succeeded) - return operationState; - -#pragma warning disable CS0618 - return new OperationState - { - CreatedTime = operationState.CreatedTime, - LastUpdatedTime = operationState.LastUpdatedTime, - OperationId = operationState.OperationId, - PercentComplete = operationState.PercentComplete, - Resources = operationState.Resources, - Results = operationState.Results, - Status = OperationStatus.Completed, - Type = operationState.Type, - }; -#pragma warning restore CS0618 - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Controllers/PartitionController.cs b/src/Microsoft.Health.Dicom.Api/Controllers/PartitionController.cs deleted file mode 100644 index 302dd3bf4e..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Controllers/PartitionController.cs +++ /dev/null @@ -1,64 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Net; -using System.Threading.Tasks; -using EnsureThat; -using MediatR; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Health.Api.Features.Audit; -using Microsoft.Health.Dicom.Api.Features.Routing; -using Microsoft.Health.Dicom.Api.Models; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Audit; -using Microsoft.Health.Dicom.Core.Web; -using DicomAudit = Microsoft.Health.Dicom.Api.Features.Audit; - -namespace Microsoft.Health.Dicom.Api.Controllers; - -[ServiceFilter(typeof(DicomAudit.AuditLoggingFilterAttribute))] -public class PartitionController : ControllerBase -{ - private readonly IMediator _mediator; - private readonly ILogger _logger; - private readonly bool _featureEnabled; - - public PartitionController(IMediator mediator, ILogger logger, IOptions featureConfiguration) - { - EnsureArg.IsNotNull(mediator, nameof(mediator)); - EnsureArg.IsNotNull(logger, nameof(logger)); - EnsureArg.IsNotNull(featureConfiguration?.Value, nameof(featureConfiguration)); - - _mediator = mediator; - _logger = logger; - _featureEnabled = featureConfiguration.Value.EnableDataPartitions; - } - - [HttpGet] - [Produces(KnownContentTypes.ApplicationJson)] - [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] - [ProducesResponseType((int)HttpStatusCode.NoContent)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)] - [VersionedRoute(KnownRoutes.GetAllPartitionsRoute)] - [AuditEventType(AuditEventSubType.Partition)] - public async Task GetAllPartitions() - { - if (!_featureEnabled) - { - throw new DataPartitionsFeatureDisabledException(); - } - - _logger.LogInformation("DICOM Web Get partitions request received to get all partitions"); - - var response = await _mediator.GetPartitionsAsync(cancellationToken: HttpContext.RequestAborted); - - return StatusCode((int)HttpStatusCode.OK, response.Entries); - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Controllers/QueryController.cs b/src/Microsoft.Health.Dicom.Api/Controllers/QueryController.cs deleted file mode 100644 index 3ea775a30f..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Controllers/QueryController.cs +++ /dev/null @@ -1,182 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using EnsureThat; -using FellowOakDicom; -using MediatR; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Microsoft.Health.Api.Features.Audit; -using Microsoft.Health.Dicom.Api.Extensions; -using Microsoft.Health.Dicom.Api.Features.Filters; -using Microsoft.Health.Dicom.Api.Features.Routing; -using Microsoft.Health.Dicom.Api.Models; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Audit; -using Microsoft.Health.Dicom.Core.Features.Query; -using Microsoft.Health.Dicom.Core.Messages.Query; -using Microsoft.Health.Dicom.Core.Web; -using DicomAudit = Microsoft.Health.Dicom.Api.Features.Audit; - -namespace Microsoft.Health.Dicom.Api.Controllers; - -[QueryModelStateValidator] -[ServiceFilter(typeof(DicomAudit.AuditLoggingFilterAttribute))] -[ServiceFilter(typeof(PopulateDataPartitionFilterAttribute))] -public class QueryController : ControllerBase -{ - private readonly IMediator _mediator; - private readonly ILogger _logger; - - public QueryController(IMediator mediator, ILogger logger) - { - EnsureArg.IsNotNull(mediator, nameof(mediator)); - EnsureArg.IsNotNull(logger, nameof(logger)); - - _mediator = mediator; - _logger = logger; - } - - [HttpGet] - [AcceptContentFilter(new[] { KnownContentTypes.ApplicationDicomJson })] - [Produces(KnownContentTypes.ApplicationDicomJson)] - [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] - [ProducesResponseType((int)HttpStatusCode.NoContent)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)] - [VersionedPartitionRoute(KnownRoutes.QueryAllStudiesRoute)] - [VersionedRoute(KnownRoutes.QueryAllStudiesRoute)] - [AuditEventType(AuditEventSubType.Query)] - public async Task QueryForStudyAsync([FromQuery] QueryOptions options) - { - _logger.LogInformation("DICOM Web Query Study request received."); - - EnsureArg.IsNotNull(options); - var response = await _mediator.QueryDicomResourcesAsync( - options.ToQueryParameters(Request.Query, QueryResource.AllStudies), - cancellationToken: HttpContext.RequestAborted); - - return CreateResult(response); - } - - [HttpGet] - [AcceptContentFilter(new[] { KnownContentTypes.ApplicationDicomJson })] - [Produces(KnownContentTypes.ApplicationDicomJson)] - [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] - [ProducesResponseType((int)HttpStatusCode.NoContent)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)] - [VersionedPartitionRoute(KnownRoutes.QueryAllSeriesRoute)] - [VersionedRoute(KnownRoutes.QueryAllSeriesRoute)] - [AuditEventType(AuditEventSubType.Query)] - public async Task QueryForSeriesAsync([FromQuery] QueryOptions options) - { - _logger.LogInformation("DICOM Web Query Series request received."); - - EnsureArg.IsNotNull(options); - var response = await _mediator.QueryDicomResourcesAsync( - options.ToQueryParameters(Request.Query, QueryResource.AllSeries), - cancellationToken: HttpContext.RequestAborted); - - return CreateResult(response); - } - - [HttpGet] - [AcceptContentFilter(new[] { KnownContentTypes.ApplicationDicomJson })] - [Produces(KnownContentTypes.ApplicationDicomJson)] - [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] - [ProducesResponseType((int)HttpStatusCode.NoContent)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)] - [VersionedPartitionRoute(KnownRoutes.QuerySeriesInStudyRoute)] - [VersionedRoute(KnownRoutes.QuerySeriesInStudyRoute)] - [AuditEventType(AuditEventSubType.Query)] - public async Task QueryForSeriesInStudyAsync([FromRoute] string studyInstanceUid, [FromQuery] QueryOptions options) - { - _logger.LogInformation("DICOM Web Query Series request for study {StudyInstanceUid} received.", studyInstanceUid); - - EnsureArg.IsNotNull(options); - var response = await _mediator.QueryDicomResourcesAsync( - options.ToQueryParameters(Request.Query, QueryResource.StudySeries, studyInstanceUid: studyInstanceUid), - cancellationToken: HttpContext.RequestAborted); - - return CreateResult(response); - } - - [HttpGet] - [AcceptContentFilter(new[] { KnownContentTypes.ApplicationDicomJson })] - [Produces(KnownContentTypes.ApplicationDicomJson)] - [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] - [ProducesResponseType((int)HttpStatusCode.NoContent)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)] - [VersionedPartitionRoute(KnownRoutes.QueryAllInstancesRoute)] - [VersionedRoute(KnownRoutes.QueryAllInstancesRoute)] - [AuditEventType(AuditEventSubType.Query)] - public async Task QueryForInstancesAsync([FromQuery] QueryOptions options) - { - _logger.LogInformation("DICOM Web Query instances request received."); - - EnsureArg.IsNotNull(options); - var response = await _mediator.QueryDicomResourcesAsync( - options.ToQueryParameters(Request.Query, QueryResource.AllInstances), - cancellationToken: HttpContext.RequestAborted); - - return CreateResult(response); - } - - [HttpGet] - [AcceptContentFilter(new[] { KnownContentTypes.ApplicationDicomJson })] - [Produces(KnownContentTypes.ApplicationDicomJson)] - [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] - [ProducesResponseType((int)HttpStatusCode.NoContent)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)] - [VersionedPartitionRoute(KnownRoutes.QueryInstancesInStudyRoute)] - [VersionedRoute(KnownRoutes.QueryInstancesInStudyRoute)] - [AuditEventType(AuditEventSubType.Query)] - public async Task QueryForInstancesInStudyAsync([FromRoute] string studyInstanceUid, [FromQuery] QueryOptions options) - { - _logger.LogInformation("DICOM Web Query Instances for study {StudyInstanceUid} received.", studyInstanceUid); - - EnsureArg.IsNotNull(options); - var response = await _mediator.QueryDicomResourcesAsync( - options.ToQueryParameters(Request.Query, QueryResource.StudyInstances, studyInstanceUid: studyInstanceUid), - cancellationToken: HttpContext.RequestAborted); - - return CreateResult(response); - } - - [HttpGet] - [AcceptContentFilter(new[] { KnownContentTypes.ApplicationDicomJson })] - [Produces(KnownContentTypes.ApplicationDicomJson)] - [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] - [ProducesResponseType((int)HttpStatusCode.NoContent)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)] - [VersionedPartitionRoute(KnownRoutes.QueryInstancesInSeriesRoute)] - [VersionedRoute(KnownRoutes.QueryInstancesInSeriesRoute)] - [AuditEventType(AuditEventSubType.Query)] - public async Task QueryForInstancesInSeriesAsync([FromRoute] string studyInstanceUid, [FromRoute] string seriesInstanceUid, [FromQuery] QueryOptions options) - { - _logger.LogInformation("DICOM Web Query Instances for study {StudyInstanceUid} and series {SeriesInstanceUid} received.", studyInstanceUid, seriesInstanceUid); - - EnsureArg.IsNotNull(options); - var response = await _mediator.QueryDicomResourcesAsync( - options.ToQueryParameters(Request.Query, QueryResource.StudySeriesInstances, studyInstanceUid: studyInstanceUid, seriesInstanceUid: seriesInstanceUid), - cancellationToken: HttpContext.RequestAborted); - - return CreateResult(response); - } - - private IActionResult CreateResult(QueryResourceResponse resourceResponse) - { - Response.TryAddErroneousAttributesHeader(resourceResponse.ErroneousTags); - if (!resourceResponse.ResponseDataset.Any()) - { - return NoContent(); - } - - return StatusCode((int)HttpStatusCode.OK, resourceResponse.ResponseDataset); - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Controllers/RetrieveController.cs b/src/Microsoft.Health.Dicom.Api/Controllers/RetrieveController.cs deleted file mode 100644 index fa95533c53..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Controllers/RetrieveController.cs +++ /dev/null @@ -1,260 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Net; -using System.Threading.Tasks; -using EnsureThat; -using FellowOakDicom; -using MediatR; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Health.Api.Features.Audit; -using Microsoft.Health.Dicom.Api.Extensions; -using Microsoft.Health.Dicom.Api.Features.Filters; -using Microsoft.Health.Dicom.Api.Features.ModelBinders; -using Microsoft.Health.Dicom.Api.Features.Responses; -using Microsoft.Health.Dicom.Api.Features.Routing; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Audit; -using Microsoft.Health.Dicom.Core.Messages; -using Microsoft.Health.Dicom.Core.Messages.Retrieve; -using Microsoft.Health.Dicom.Core.Web; -using DicomAudit = Microsoft.Health.Dicom.Api.Features.Audit; - -namespace Microsoft.Health.Dicom.Api.Controllers; - -[QueryModelStateValidator] -[ServiceFilter(typeof(DicomAudit.AuditLoggingFilterAttribute))] -[ServiceFilter(typeof(PopulateDataPartitionFilterAttribute))] -public class RetrieveController : ControllerBase -{ - private readonly IMediator _mediator; - private readonly ILogger _logger; - private readonly RetrieveConfiguration _retrieveConfiguration; - private const string IfNoneMatch = "If-None-Match"; - - public RetrieveController(IMediator mediator, ILogger logger, IOptionsSnapshot retrieveConfiguration) - { - EnsureArg.IsNotNull(mediator, nameof(mediator)); - EnsureArg.IsNotNull(logger, nameof(logger)); - EnsureArg.IsNotNull(retrieveConfiguration?.Value, nameof(retrieveConfiguration)); - - _mediator = mediator; - _logger = logger; - _retrieveConfiguration = retrieveConfiguration.Value; - } - - [Produces(KnownContentTypes.MultipartRelated)] - [ProducesResponseType((int)HttpStatusCode.OK)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.NotFound)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.NotAcceptable)] - [HttpGet] - [VersionedPartitionRoute(KnownRoutes.StudyRoute, Name = KnownRouteNames.PartitionRetrieveStudy)] - [VersionedRoute(KnownRoutes.StudyRoute, Name = KnownRouteNames.RetrieveStudy)] - [AuditEventType(AuditEventSubType.Retrieve)] - public async Task GetStudyAsync(string studyInstanceUid) - { - _logger.LogInformation("DICOM Web Retrieve Transaction request received, for study: {StudyInstanceUid}.", studyInstanceUid); - - RetrieveResourceResponse response = await _mediator.RetrieveDicomStudyAsync(studyInstanceUid, HttpContext.Request.GetAcceptHeaders(), HttpContext.Request.IsOriginalVersionRequested(), HttpContext.RequestAborted); - - return CreateResult(response); - } - - [AcceptContentFilter(new[] { KnownContentTypes.ApplicationDicomJson })] - [Produces(KnownContentTypes.ApplicationDicomJson)] - [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.NotFound)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.NotAcceptable)] - [ProducesResponseType((int)HttpStatusCode.NotModified)] - [HttpGet] - [VersionedPartitionRoute(KnownRoutes.StudyMetadataRoute)] - [VersionedRoute(KnownRoutes.StudyMetadataRoute)] - [AuditEventType(AuditEventSubType.RetrieveMetadata)] - public async Task GetStudyMetadataAsync([FromHeader(Name = IfNoneMatch)] string ifNoneMatch, string studyInstanceUid) - { - _logger.LogInformation("DICOM Web Retrieve Metadata Transaction request received, for study: {StudyInstanceUid}.", studyInstanceUid); - - RetrieveMetadataResponse response = await _mediator.RetrieveDicomStudyMetadataAsync(studyInstanceUid, ifNoneMatch, HttpContext.Request.IsOriginalVersionRequested(), HttpContext.RequestAborted); - - return CreateResult(response); - } - - [Produces(KnownContentTypes.MultipartRelated)] - [ProducesResponseType((int)HttpStatusCode.OK)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.NotFound)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.NotAcceptable)] - [HttpGet] - [VersionedPartitionRoute(KnownRoutes.SeriesRoute, Name = KnownRouteNames.PartitionRetrieveSeries)] - [VersionedRoute(KnownRoutes.SeriesRoute, Name = KnownRouteNames.RetrieveSeries)] - [AuditEventType(AuditEventSubType.Retrieve)] - public async Task GetSeriesAsync( - string studyInstanceUid, - string seriesInstanceUid) - { - _logger.LogInformation("DICOM Web Retrieve Transaction request received, for study: {StudyInstanceUid}, series: {SeriesInstanceUid}.", studyInstanceUid, seriesInstanceUid); - - RetrieveResourceResponse response = await _mediator.RetrieveDicomSeriesAsync( - studyInstanceUid, seriesInstanceUid, HttpContext.Request.GetAcceptHeaders(), HttpContext.Request.IsOriginalVersionRequested(), HttpContext.RequestAborted); - - return CreateResult(response); - } - - [AcceptContentFilter(new[] { KnownContentTypes.ApplicationDicomJson })] - [Produces(KnownContentTypes.ApplicationDicomJson)] - [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.NotFound)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.NotAcceptable)] - [ProducesResponseType((int)HttpStatusCode.NotModified)] - [HttpGet] - [VersionedPartitionRoute(KnownRoutes.SeriesMetadataRoute)] - [VersionedRoute(KnownRoutes.SeriesMetadataRoute)] - [AuditEventType(AuditEventSubType.RetrieveMetadata)] - public async Task GetSeriesMetadataAsync([FromHeader(Name = IfNoneMatch)] string ifNoneMatch, string studyInstanceUid, string seriesInstanceUid) - { - _logger.LogInformation("DICOM Web Retrieve Metadata Transaction request received, for study: {StudyInstanceUid}, series: {SeriesInstanceUid}.", studyInstanceUid, seriesInstanceUid); - - RetrieveMetadataResponse response = await _mediator.RetrieveDicomSeriesMetadataAsync( - studyInstanceUid, seriesInstanceUid, ifNoneMatch, HttpContext.Request.IsOriginalVersionRequested(), HttpContext.RequestAborted); - - return CreateResult(response); - } - - [Produces(KnownContentTypes.ApplicationDicom, KnownContentTypes.MultipartRelated)] - [ProducesResponseType((int)HttpStatusCode.OK)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.NotFound)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.NotAcceptable)] - [HttpGet] - [VersionedPartitionRoute(KnownRoutes.InstanceRoute, Name = KnownRouteNames.PartitionRetrieveInstance)] - [VersionedRoute(KnownRoutes.InstanceRoute, Name = KnownRouteNames.RetrieveInstance)] - [AuditEventType(AuditEventSubType.Retrieve)] - public async Task GetInstanceAsync( - string studyInstanceUid, - string seriesInstanceUid, - string sopInstanceUid) - { - _logger.LogInformation("DICOM Web Retrieve Transaction request received, for study: '{StudyInstanceUid}', series: '{SeriesInstanceUid}', instance: '{SopInstanceUid}'.", studyInstanceUid, seriesInstanceUid, sopInstanceUid); - - RetrieveResourceResponse response = await _mediator.RetrieveDicomInstanceAsync( - studyInstanceUid, seriesInstanceUid, sopInstanceUid, HttpContext.Request.GetAcceptHeaders(), HttpContext.Request.IsOriginalVersionRequested(), HttpContext.RequestAborted); - - return CreateResult(response); - } - - [Produces(KnownContentTypes.ImageJpeg, KnownContentTypes.ImagePng)] - [ProducesResponseType((int)HttpStatusCode.OK)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.NoContent)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.NotFound)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.NotAcceptable)] - [HttpGet] - [VersionedPartitionRoute(KnownRoutes.InstanceRenderedRoute)] - [VersionedRoute(KnownRoutes.InstanceRenderedRoute)] - [AuditEventType(AuditEventSubType.RetrieveRendered)] - public async Task GetRenderedInstanceAsync( - string studyInstanceUid, - string seriesInstanceUid, - string sopInstanceUid, - [FromQuery] int quality = 100) - { - _logger.LogInformation("DICOM Web Retrieve Rendered Image Transaction request for instance received"); - - RetrieveRenderedResponse response = await _mediator.RetrieveRenderedDicomInstanceAsync( - studyInstanceUid, seriesInstanceUid, sopInstanceUid, ResourceType.Instance, HttpContext.Request.GetAcceptHeaders(), quality, HttpContext.RequestAborted); - - return CreateResult(response); - } - - [AcceptContentFilter(new[] { KnownContentTypes.ApplicationDicomJson })] - [Produces(KnownContentTypes.ApplicationDicomJson)] - [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.NotFound)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.NotAcceptable)] - [ProducesResponseType((int)HttpStatusCode.NotModified)] - [HttpGet] - [VersionedPartitionRoute(KnownRoutes.InstanceMetadataRoute)] - [VersionedRoute(KnownRoutes.InstanceMetadataRoute)] - [AuditEventType(AuditEventSubType.RetrieveMetadata)] - public async Task GetInstanceMetadataAsync( - [FromHeader(Name = IfNoneMatch)] string ifNoneMatch, - string studyInstanceUid, - string seriesInstanceUid, - string sopInstanceUid) - { - _logger.LogInformation("DICOM Web Retrieve Metadata Transaction request received, for study: {StudyInstanceUid}, series: {SeriesInstanceUid}, instance: {SopInstanceUid}.", studyInstanceUid, seriesInstanceUid, sopInstanceUid); - - RetrieveMetadataResponse response = await _mediator.RetrieveDicomInstanceMetadataAsync( - studyInstanceUid, seriesInstanceUid, sopInstanceUid, ifNoneMatch, HttpContext.Request.IsOriginalVersionRequested(), HttpContext.RequestAborted); - - return CreateResult(response); - } - - [Produces(KnownContentTypes.ApplicationDicom, KnownContentTypes.MultipartRelated)] - [ProducesResponseType((int)HttpStatusCode.OK)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.NotFound)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.NotAcceptable)] - [HttpGet] - [VersionedPartitionRoute(KnownRoutes.FrameRoute, Name = KnownRouteNames.PartitionRetrieveFrame)] - [VersionedRoute(KnownRoutes.FrameRoute, Name = KnownRouteNames.RetrieveFrame)] - [AuditEventType(AuditEventSubType.Retrieve)] - public async Task GetFramesAsync( - string studyInstanceUid, - string seriesInstanceUid, - string sopInstanceUid, - [FromRoute][ModelBinder(typeof(IntArrayModelBinder))] int[] frames) - { - _logger.LogInformation("DICOM Web Retrieve Transaction request received, for study: {StudyInstanceUid}, series: {SeriesInstanceUid}, instance: {SopInstanceUid}, frames: {Frames}.", studyInstanceUid, seriesInstanceUid, sopInstanceUid, string.Join(", ", frames ?? Array.Empty())); - RetrieveResourceResponse response = await _mediator.RetrieveDicomFramesAsync( - studyInstanceUid, seriesInstanceUid, sopInstanceUid, frames, HttpContext.Request.GetAcceptHeaders(), HttpContext.RequestAborted); - - return CreateResult(response); - } - - [Produces(KnownContentTypes.ImageJpeg, KnownContentTypes.ImagePng)] - [ProducesResponseType((int)HttpStatusCode.OK)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.NoContent)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.NotFound)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.NotAcceptable)] - [HttpGet] - [VersionedPartitionRoute(KnownRoutes.FrameRenderedRoute)] - [VersionedRoute(KnownRoutes.FrameRenderedRoute)] - [AuditEventType(AuditEventSubType.RetrieveRendered)] - public async Task GetRenderedFrameAsync( - string studyInstanceUid, - string seriesInstanceUid, - string sopInstanceUid, - int frame, - [FromQuery] int quality = 100) - { - _logger.LogInformation("DICOM Web Retrieve Rendered Image Transaction request for frame received"); - - RetrieveRenderedResponse response = await _mediator.RetrieveRenderedDicomInstanceAsync( - studyInstanceUid, seriesInstanceUid, sopInstanceUid, ResourceType.Frames, HttpContext.Request.GetAcceptHeaders(), quality, HttpContext.RequestAborted, frame); - - return CreateResult(response); - } - - private ResourceResult CreateResult(RetrieveResourceResponse response) - => new ResourceResult(response, _retrieveConfiguration); - - private static MetadataResult CreateResult(RetrieveMetadataResponse response) - => new MetadataResult(response); - - private static RenderedResult CreateResult(RetrieveRenderedResponse response) - => new RenderedResult(response); -} diff --git a/src/Microsoft.Health.Dicom.Api/Controllers/StoreController.cs b/src/Microsoft.Health.Dicom.Api/Controllers/StoreController.cs deleted file mode 100644 index 8a135ce5f4..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Controllers/StoreController.cs +++ /dev/null @@ -1,144 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.ComponentModel.DataAnnotations; -using System.Net; -using System.Threading.Tasks; -using EnsureThat; -using FellowOakDicom; -using MediatR; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Health.Api.Features.Audit; -using Microsoft.Health.Dicom.Api.Extensions; -using Microsoft.Health.Dicom.Api.Features.Conventions; -using Microsoft.Health.Dicom.Api.Features.Filters; -using Microsoft.Health.Dicom.Api.Features.Routing; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Audit; -using Microsoft.Health.Dicom.Core.Features.Context; -using Microsoft.Health.Dicom.Core.Messages.Store; -using Microsoft.Health.Dicom.Core.Messages.Update; -using Microsoft.Health.Dicom.Core.Models; -using Microsoft.Health.Dicom.Core.Models.Update; -using Microsoft.Health.Dicom.Core.Web; -using Microsoft.Health.Operations; -using DicomAudit = Microsoft.Health.Dicom.Api.Features.Audit; - -namespace Microsoft.Health.Dicom.Api.Controllers; - -[QueryModelStateValidator] -[ServiceFilter(typeof(DicomAudit.AuditLoggingFilterAttribute))] -[ServiceFilter(typeof(PopulateDataPartitionFilterAttribute))] -public class StoreController : ControllerBase -{ - private readonly IDicomRequestContextAccessor _dicomRequestContextAccessor; - private readonly IMediator _mediator; - private readonly ILogger _logger; - private readonly bool _asyncOperationDisabled; - - public StoreController(IMediator mediator, ILogger logger, IOptions featureConfiguration, IDicomRequestContextAccessor dicomRequestContextAccessor) - { - EnsureArg.IsNotNull(mediator, nameof(mediator)); - EnsureArg.IsNotNull(logger, nameof(logger)); - EnsureArg.IsNotNull(featureConfiguration, nameof(featureConfiguration)); - EnsureArg.IsNotNull(dicomRequestContextAccessor, nameof(dicomRequestContextAccessor)); - - _mediator = mediator; - _logger = logger; - _asyncOperationDisabled = featureConfiguration.Value.DisableOperations; - _dicomRequestContextAccessor = dicomRequestContextAccessor; - } - - [AcceptContentFilter(new[] { KnownContentTypes.ApplicationDicomJson })] - [Produces(KnownContentTypes.ApplicationDicomJson)] - [Consumes(KnownContentTypes.ApplicationDicom, KnownContentTypes.MultipartRelated)] - [ProducesResponseType(typeof(DicomDataset), (int)HttpStatusCode.OK)] - [ProducesResponseType(typeof(DicomDataset), (int)HttpStatusCode.Accepted)] - [ProducesResponseType((int)HttpStatusCode.NoContent)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.NotAcceptable)] - [ProducesResponseType(typeof(DicomDataset), (int)HttpStatusCode.Conflict)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.UnsupportedMediaType)] - [HttpPost] - [VersionedPartitionRoute(KnownRoutes.StoreInstancesRoute, Name = KnownRouteNames.PartitionStoreInstance)] - [VersionedRoute(KnownRoutes.StoreInstancesRoute, Name = KnownRouteNames.StoreInstance)] - [AuditEventType(AuditEventSubType.Store)] - public async Task PostInstanceAsync() - { - return await PostAsync(null); - } - - [AcceptContentFilter(new[] { KnownContentTypes.ApplicationDicomJson })] - [Produces(KnownContentTypes.ApplicationDicomJson)] - [Consumes(KnownContentTypes.ApplicationDicom, KnownContentTypes.MultipartRelated)] - [ProducesResponseType(typeof(DicomDataset), (int)HttpStatusCode.OK)] - [ProducesResponseType(typeof(DicomDataset), (int)HttpStatusCode.Accepted)] - [ProducesResponseType((int)HttpStatusCode.NoContent)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.NotAcceptable)] - [ProducesResponseType(typeof(DicomDataset), (int)HttpStatusCode.Conflict)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.UnsupportedMediaType)] - [HttpPost] - [VersionedPartitionRoute(KnownRoutes.StoreInstancesInStudyRoute, Name = KnownRouteNames.PartitionStoreInstancesInStudy)] - [VersionedRoute(KnownRoutes.StoreInstancesInStudyRoute, Name = KnownRouteNames.StoreInstancesInStudy)] - [AuditEventType(AuditEventSubType.Store)] - public async Task PostInstanceInStudyAsync(string studyInstanceUid) - { - return await PostAsync(studyInstanceUid); - } - - [HttpPost] - [IntroducedInApiVersion(2)] - [Consumes(KnownContentTypes.ApplicationJson)] - [ProducesResponseType(typeof(OperationReference), (int)HttpStatusCode.Accepted)] - [ProducesResponseType(typeof(DicomDataset), (int)HttpStatusCode.BadRequest)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.NotFound)] - [VersionedPartitionRoute(KnownRoutes.UpdateInstanceRoute, Name = KnownRouteNames.PartitionedUpdateInstance)] - [VersionedRoute(KnownRoutes.UpdateInstanceRoute, Name = KnownRouteNames.UpdateInstance)] - [AuditEventType(AuditEventSubType.UpdateStudy)] - public async Task UpdateAsync([FromBody][Required] UpdateSpecification updateSpecification) - { - if (_asyncOperationDisabled) - { - throw new DicomAsyncOperationDisabledException(); - } - - // DICOM update only supported in API version 2 and above - if (_dicomRequestContextAccessor.RequestContext.Version < ApiVersionsConvention.MinimumSupportedVersionForDicomUpdate) - { - return StatusCode((int)HttpStatusCode.NotFound); - } - - UpdateInstanceResponse response = await _mediator.UpdateInstanceAsync(updateSpecification); - if (response.FailedDataset != null) - { - return StatusCode((int)HttpStatusCode.BadRequest, response.FailedDataset); - } - return StatusCode((int)HttpStatusCode.Accepted, response.Operation); - } - - private async Task PostAsync(string studyInstanceUid) - { - _logger.LogInformation("DICOM Web Store Transaction request received, with study instance UID {StudyInstanceUid}", studyInstanceUid); - - StoreResponse storeResponse = await _mediator.StoreDicomResourcesAsync( - Request.Body, - Request.ContentType, - studyInstanceUid, - HttpContext.RequestAborted); - if (!string.IsNullOrEmpty(storeResponse.Warning)) - { - Response.SetWarning(HttpWarningCode.MiscPersistentWarning, Request.GetHost(dicomStandards: true), storeResponse.Warning); - } - - return StatusCode( - (int)storeResponse.Status.ToHttpStatusCode(), - storeResponse.Dataset); - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Controllers/WorkitemController.Add.cs b/src/Microsoft.Health.Dicom.Api/Controllers/WorkitemController.Add.cs deleted file mode 100644 index ceed459711..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Controllers/WorkitemController.Add.cs +++ /dev/null @@ -1,79 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using FellowOakDicom; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Microsoft.Health.Api.Features.Audit; -using Microsoft.Health.Dicom.Api.Extensions; -using Microsoft.Health.Dicom.Api.Features.Filters; -using Microsoft.Health.Dicom.Api.Features.Routing; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Audit; -using Microsoft.Health.Dicom.Core.Messages.Workitem; -using Microsoft.Health.Dicom.Core.Web; -using Microsoft.Net.Http.Headers; - -namespace Microsoft.Health.Dicom.Api.Controllers; - -public partial class WorkitemController -{ - /// - /// This action requests the creation of a UPS Instance on the Origin-Server. It corresponds to the UPS DIMSE N-CREATE operation. - /// - /// - /// The request body contains all the metadata to be stored in DICOM PS 3.18 JSON metadata. - /// Any binary data contained in the message shall be inline. - /// - /// DICOM PS 3.19 XML metadata is not supported. - /// - /// - [AcceptContentFilter(new[] { KnownContentTypes.ApplicationDicomJson })] - [Produces(KnownContentTypes.ApplicationDicomJson)] - [Consumes(KnownContentTypes.ApplicationDicomJson)] - [ProducesResponseType((int)HttpStatusCode.OK)] - [ProducesResponseType((int)HttpStatusCode.Accepted)] - [ProducesResponseType((int)HttpStatusCode.Conflict)] - [ProducesResponseType((int)HttpStatusCode.NoContent)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.NotAcceptable)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.UnsupportedMediaType)] - [HttpPost] - [VersionedPartitionRoute(KnownRoutes.AddWorkitemInstancesRoute, Name = KnownRouteNames.PartitionedAddWorkitemInstance)] - [VersionedRoute(KnownRoutes.AddWorkitemInstancesRoute, Name = KnownRouteNames.AddWorkitemInstance)] - [AuditEventType(AuditEventSubType.AddWorkitem)] - public async Task AddAsync([FromBody][Required][MinLength(1)] IReadOnlyList dicomDatasets) - { - // The Workitem UID is passed as the name of the first query parameter - string workitemUid = HttpContext.Request.Query.Keys.FirstOrDefault(); - - return await PostAddAsync(workitemUid, dicomDatasets); - } - - private async Task PostAddAsync(string workitemInstanceUid, IReadOnlyList dicomDatasets) - { - _logger.LogInformation("DICOM Web Add Workitem Transaction request received with file size of {FileSize} bytes.", Request.ContentLength); - - AddWorkitemResponse response = await _mediator.AddWorkitemAsync( - dicomDatasets[0], - Request.ContentType, - workitemInstanceUid, - HttpContext.RequestAborted); - - if (response.Status == WorkitemResponseStatus.Success) - { - Response.Headers.Append(HeaderNames.ContentLocation, response.Uri.ToString()); - Response.Headers.Append(HeaderNames.Location, response.Uri.ToString()); - } - - return StatusCode((int)response.Status.AddResponseToHttpStatusCode(), response.Message); - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Controllers/WorkitemController.Cancel.cs b/src/Microsoft.Health.Dicom.Api/Controllers/WorkitemController.Cancel.cs deleted file mode 100644 index e787ad3ce4..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Controllers/WorkitemController.Cancel.cs +++ /dev/null @@ -1,74 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Net; -using System.Threading.Tasks; -using FellowOakDicom; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Microsoft.Health.Api.Features.Audit; -using Microsoft.Health.Dicom.Api.Extensions; -using Microsoft.Health.Dicom.Api.Features.Routing; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Audit; -using Microsoft.Health.Dicom.Core.Models; -using Microsoft.Health.Dicom.Core.Web; - -namespace Microsoft.Health.Dicom.Api.Controllers; - -public partial class WorkitemController -{ - /// - /// RequestUPSCancellation - /// This action requests the cancellation of a UPS Instance managed by the Origin-Server. - /// It corresponds to the UPS DIMSE N-ACTION operation "Request UPS Cancel". - /// - /// - /// This resource records a request that the specified UPS Instance be canceled. - /// - /// This transaction allows a user agent that does not own a Workitem to request that it be canceled. - /// It corresponds to the UPS DIMSE N-ACTION operation "Request UPS Cancel". See Section CC.2.2 in PS3.4 . - /// - /// To cancel a Workitem that the user agent owns, i.e., that is in the IN PROGRESS state, - /// the user agent uses the Change Workitem State transaction as described in Section 11.7. - /// - /// - /// The workitem Uid - /// The DICOM dataset payload in the body. - /// Returns a string status report. - [HttpPost] - [Produces(KnownContentTypes.ApplicationDicomJson)] - [Consumes(KnownContentTypes.ApplicationDicomJson)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.Accepted)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.Conflict)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.NotFound)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.UnsupportedMediaType)] - [VersionedPartitionRoute(KnownRoutes.CancelWorkitemInstancesRoute, Name = KnownRouteNames.PartitionedCancelWorkitemInstance)] - [VersionedRoute(KnownRoutes.CancelWorkitemInstancesRoute, Name = KnownRouteNames.CancelWorkitemInstance)] - [AuditEventType(AuditEventSubType.CancelWorkitem)] - public async Task CancelAsync(string workitemInstanceUid, [FromBody][Required][MinLength(1)] IReadOnlyList dicomDatasets) - { - _logger.LogInformation("DICOM Web Cancel Workitem Transaction request received with file size of {FileSize} bytes.", Request.ContentLength); - - var response = await _mediator.CancelWorkitemAsync( - dicomDatasets[0], - Request.ContentType, - workitemInstanceUid, - HttpContext.RequestAborted) - .ConfigureAwait(false); - - if (response.Status is Core.Messages.Workitem.WorkitemResponseStatus.Conflict - && !string.IsNullOrEmpty(response.Message)) - { - Response.SetWarning(HttpWarningCode.MiscPersistentWarning, Request.GetHost(dicomStandards: true), response.Message); - } - - return StatusCode((int)response.Status.CancelResponseToHttpStatusCode(), response.Message); - } - -} diff --git a/src/Microsoft.Health.Dicom.Api/Controllers/WorkitemController.ChangeState.cs b/src/Microsoft.Health.Dicom.Api/Controllers/WorkitemController.ChangeState.cs deleted file mode 100644 index 6129d4a9f5..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Controllers/WorkitemController.ChangeState.cs +++ /dev/null @@ -1,58 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Net; -using System.Threading.Tasks; -using FellowOakDicom; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Microsoft.Health.Api.Features.Audit; -using Microsoft.Health.Dicom.Api.Extensions; -using Microsoft.Health.Dicom.Api.Features.Routing; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Audit; -using Microsoft.Health.Dicom.Core.Models; -using Microsoft.Health.Dicom.Core.Web; - -namespace Microsoft.Health.Dicom.Api.Controllers; - -public partial class WorkitemController -{ - /// - /// This transaction is used to change the state of a Workitem. - /// It corresponds to the UPS DIMSE N-ACTION operation "Change UPS State". - /// State changes are used to claim ownership, complete, or cancel a Workitem. - /// - [HttpPut] - [Consumes(KnownContentTypes.ApplicationDicomJson)] - [ProducesResponseType((int)HttpStatusCode.OK)] - [ProducesResponseType((int)HttpStatusCode.BadRequest)] - [ProducesResponseType((int)HttpStatusCode.NotFound)] - [ProducesResponseType((int)HttpStatusCode.Conflict)] - [VersionedPartitionRoute(KnownRoutes.ChangeStateWorkitemInstancesRoute, Name = KnownRouteNames.PartitionChangeStateWorkitemInstance)] - [VersionedRoute(KnownRoutes.ChangeStateWorkitemInstancesRoute, Name = KnownRouteNames.ChangeStateWorkitemInstance)] - [AuditEventType(AuditEventSubType.ChangeStateWorkitem)] - public async Task ChangeStateAsync(string workitemInstanceUid, [FromBody][Required][MinLength(1)] IReadOnlyList dicomDatasets) - { - _logger.LogInformation("DICOM Web ChangeState Workitem Transaction request received with file size of {FileSize} bytes.", Request.ContentLength); - - var response = await _mediator - .ChangeWorkitemStateAsync( - dicomDatasets[0], - Request.ContentType, - workitemInstanceUid, - cancellationToken: HttpContext.RequestAborted) - .ConfigureAwait(false); - - if (!string.IsNullOrWhiteSpace(response.Message)) - { - Response.SetWarning(HttpWarningCode.MiscPersistentWarning, Request.GetHost(dicomStandards: true), response.Message); - } - - return StatusCode((int)response.Status.ChangeStateResponseToHttpStatusCode(), response.Message); - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Controllers/WorkitemController.Query.cs b/src/Microsoft.Health.Dicom.Api/Controllers/WorkitemController.Query.cs deleted file mode 100644 index 441fdafb0e..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Controllers/WorkitemController.Query.cs +++ /dev/null @@ -1,53 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using EnsureThat; -using FellowOakDicom; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Microsoft.Health.Api.Features.Audit; -using Microsoft.Health.Dicom.Api.Extensions; -using Microsoft.Health.Dicom.Api.Features.Filters; -using Microsoft.Health.Dicom.Api.Features.Routing; -using Microsoft.Health.Dicom.Api.Models; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Audit; -using Microsoft.Health.Dicom.Core.Web; - -namespace Microsoft.Health.Dicom.Api.Controllers; - -public partial class WorkitemController -{ - /// - /// This transaction searches the Worklist for Workitems that match the specified Query Parameters and returns a list of matching Workitems. - /// Each Workitem in the returned list includes return Attributes specified in the request. The transaction corresponds to the UPS DIMSE C-FIND operation. - /// - /// ObjectResult which contains list of dicomdataset - [HttpGet] - [AcceptContentFilter(new[] { KnownContentTypes.ApplicationDicomJson })] - [Produces(KnownContentTypes.ApplicationDicomJson)] - [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] - [ProducesResponseType((int)HttpStatusCode.NoContent)] - [ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)] - [VersionedPartitionRoute(KnownRoutes.SearchWorkitemInstancesRoute, Name = KnownRouteNames.PartitionSearchWorkitemInstance)] - [VersionedRoute(KnownRoutes.SearchWorkitemInstancesRoute, Name = KnownRouteNames.SearchWorkitemInstance)] - [AuditEventType(AuditEventSubType.QueryWorkitem)] - [QueryModelStateValidator] - public async Task QueryWorkitemsAsync([FromQuery] QueryOptions options) - { - _logger.LogInformation("Query workitem request received."); - - EnsureArg.IsNotNull(options); - var response = await _mediator.QueryWorkitemsAsync( - options.ToBaseQueryParameters(Request.Query), - cancellationToken: HttpContext.RequestAborted); - - return response.ResponseDatasets.Any() ? StatusCode((int)response.Status.QueryResponseToHttpStatusCode(), response.ResponseDatasets) : NoContent(); - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Controllers/WorkitemController.Retrieve.cs b/src/Microsoft.Health.Dicom.Api/Controllers/WorkitemController.Retrieve.cs deleted file mode 100644 index 2090c60379..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Controllers/WorkitemController.Retrieve.cs +++ /dev/null @@ -1,48 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Net; -using System.Threading.Tasks; -using FellowOakDicom; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Health.Api.Features.Audit; -using Microsoft.Health.Dicom.Api.Extensions; -using Microsoft.Health.Dicom.Api.Features.Filters; -using Microsoft.Health.Dicom.Api.Features.Routing; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Audit; -using Microsoft.Health.Dicom.Core.Messages.Workitem; -using Microsoft.Health.Dicom.Core.Web; - -namespace Microsoft.Health.Dicom.Api.Controllers; - -public partial class WorkitemController -{ - /// - /// This action requests a UPS Instance on the Origin-Server. It corresponds to the UPS DIMSE N-GET operation. - /// - [HttpGet] - [AcceptContentFilter(new[] { KnownContentTypes.ApplicationDicomJson })] - [Produces(KnownContentTypes.ApplicationDicomJson)] - [ProducesResponseType(typeof(DicomDataset), (int)HttpStatusCode.OK)] - [ProducesResponseType((int)HttpStatusCode.BadRequest)] - [ProducesResponseType((int)HttpStatusCode.NotFound)] - [VersionedPartitionRoute(KnownRoutes.RetrieveWorkitemInstancesRoute, Name = KnownRouteNames.PartitionedRetrieveWorkitemInstance)] - [VersionedRoute(KnownRoutes.RetrieveWorkitemInstancesRoute, Name = KnownRouteNames.RetrieveWorkitemInstance)] - [AuditEventType(AuditEventSubType.RetrieveWorkitem)] - public async Task RetrieveAsync(string workitemInstanceUid) - { - var response = await _mediator - .RetrieveWorkitemAsync(workitemInstanceUid, cancellationToken: HttpContext.RequestAborted) - .ConfigureAwait(false); - - if (response.Status == WorkitemResponseStatus.Success) - { - return Ok(response.Dataset); - } - - return StatusCode((int)response.Status.RetrieveResponseToHttpStatusCode(), response.Message); - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Controllers/WorkitemController.Update.cs b/src/Microsoft.Health.Dicom.Api/Controllers/WorkitemController.Update.cs deleted file mode 100644 index 73fe1ea57b..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Controllers/WorkitemController.Update.cs +++ /dev/null @@ -1,77 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using FellowOakDicom; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Microsoft.Health.Api.Features.Audit; -using Microsoft.Health.Dicom.Api.Extensions; -using Microsoft.Health.Dicom.Api.Features.Routing; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Audit; -using Microsoft.Health.Dicom.Core.Messages.Workitem; -using Microsoft.Health.Dicom.Core.Models; -using Microsoft.Health.Dicom.Core.Web; -using Microsoft.Net.Http.Headers; - -namespace Microsoft.Health.Dicom.Api.Controllers; - -public partial class WorkitemController -{ - /// - /// This action requests the update of a UPS Instance on the Origin-Server. It corresponds to the UPS DIMSE N-SET operation. - /// - /// - /// The request body contains all the metadata to be updated in DICOM PS 3.18 JSON metadata. - /// - /// - [HttpPost] - [Consumes(KnownContentTypes.ApplicationDicomJson)] - [ProducesResponseType((int)HttpStatusCode.OK)] - [ProducesResponseType((int)HttpStatusCode.BadRequest)] - [ProducesResponseType((int)HttpStatusCode.Conflict)] - [ProducesResponseType((int)HttpStatusCode.NotFound)] - [ProducesResponseType((int)HttpStatusCode.UnsupportedMediaType)] - [VersionedPartitionRoute(KnownRoutes.UpdateWorkitemInstancesRoute, Name = KnownRouteNames.PartitionedUpdateWorkitemInstance)] - [VersionedRoute(KnownRoutes.UpdateWorkitemInstancesRoute, Name = KnownRouteNames.UpdateWorkitemInstance)] - [AuditEventType(AuditEventSubType.UpdateWorkitem)] - public async Task UpdateAsync(string workitemInstanceUid, [FromBody][Required][MinLength(1)] IReadOnlyList dicomDatasets) - { - // The Transaction UID is passed as the first query parameter - string transactionUid = HttpContext.Request.Query.Keys.FirstOrDefault(); - - return await PostUpdateAsync(workitemInstanceUid, transactionUid, dicomDatasets); - } - - private async Task PostUpdateAsync(string workitemInstanceUid, string transactionUid, IReadOnlyList dicomDatasets) - { - _logger.LogInformation("DICOM Web Update Workitem Transaction request received with file size of {FileSize} bytes.", Request.ContentLength); - - UpdateWorkitemResponse response = await _mediator.UpdateWorkitemAsync( - dicomDatasets[0], - Request.ContentType, - workitemInstanceUid, - transactionUid, - HttpContext.RequestAborted); - - if (response.Status == WorkitemResponseStatus.Success) - { - Response.Headers.Append(HeaderNames.ContentLocation, response.Uri.ToString()); - } - - if (!string.IsNullOrWhiteSpace(response.Message)) - { - Response.SetWarning(HttpWarningCode.MiscPersistentWarning, Request.GetHost(dicomStandards: true), response.Message); - } - - return StatusCode((int)response.Status.UpdateResponseToHttpStatusCode(), response.Message); - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Controllers/WorkitemController.cs b/src/Microsoft.Health.Dicom.Api/Controllers/WorkitemController.cs deleted file mode 100644 index 6a44d96e6c..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Controllers/WorkitemController.cs +++ /dev/null @@ -1,33 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Diagnostics.CodeAnalysis; -using EnsureThat; -using MediatR; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Microsoft.Health.Dicom.Api.Features.Filters; -using DicomAudit = Microsoft.Health.Dicom.Api.Features.Audit; - -namespace Microsoft.Health.Dicom.Api.Controllers; - -[QueryModelStateValidator] -[ServiceFilter(typeof(DicomAudit.AuditLoggingFilterAttribute))] -[ServiceFilter(typeof(PopulateDataPartitionFilterAttribute))] -[SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "Arguments are validated by RequiredAttribute")] -public partial class WorkitemController : ControllerBase -{ - private readonly IMediator _mediator; - private readonly ILogger _logger; - - public WorkitemController(IMediator mediator, ILogger logger) - { - EnsureArg.IsNotNull(mediator, nameof(mediator)); - EnsureArg.IsNotNull(logger, nameof(logger)); - - _mediator = mediator; - _logger = logger; - } -} diff --git a/src/Microsoft.Health.Dicom.Api/DicomApiResource.Designer.cs b/src/Microsoft.Health.Dicom.Api/DicomApiResource.Designer.cs deleted file mode 100644 index d34c4a50a6..0000000000 --- a/src/Microsoft.Health.Dicom.Api/DicomApiResource.Designer.cs +++ /dev/null @@ -1,216 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Microsoft.Health.Dicom.Api { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class DicomApiResource { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal DicomApiResource() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.Health.Dicom.Api.DicomApiResource", typeof(DicomApiResource).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to The maximum length of a custom audit header value is {0}. The supplied custom audit header '{1}' has length of {2}.. - /// - internal static string CustomAuditHeaderTooLarge { - get { - return ResourceManager.GetString("CustomAuditHeaderTooLarge", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid QIDO-RS query. Duplicate AttributeId '{0}'. Each attribute is only allowed to be specified once.. - /// - internal static string DuplicateAttributeId { - get { - return ResourceManager.GetString("DuplicateAttributeId", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cannot specify parameter more than once.. - /// - internal static string DuplicateParameter { - get { - return ResourceManager.GetString("DuplicateParameter", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The server encountered an internal error. Please retry the request. If the issue persists, please contact support.. - /// - internal static string InternalServerError { - get { - return ResourceManager.GetString("InternalServerError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The value is not valid. It need to be one of "{0}".. - /// - internal static string InvalidEnumValue { - get { - return ResourceManager.GetString("InvalidEnumValue", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to There was an error reading the multipart request.. - /// - internal static string InvalidMultipartBodyPart { - get { - return ResourceManager.GetString("InvalidMultipartBodyPart", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The content type '{0}' is either not multipart or is missing boundary.. - /// - internal static string InvalidMultipartContentType { - get { - return ResourceManager.GetString("InvalidMultipartContentType", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Unable to parse value '{0}' as {1}.. - /// - internal static string InvalidParse { - get { - return ResourceManager.GetString("InvalidParse", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The field '{0}' in request body is invalid: {1}. - /// - internal static string InvalidRequestBody { - get { - return ResourceManager.GetString("InvalidRequestBody", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The syntax of the request body is invalid.. - /// - internal static string InvalidSyntax { - get { - return ResourceManager.GetString("InvalidSyntax", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The start time '{0:O}' must occur strictly before the end time '{1:O}'.. - /// - internal static string InvalidTimeRange { - get { - return ResourceManager.GetString("InvalidTimeRange", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The audit information is missing for Controller: {0} and Action: {1}. This usually means the action is not marked with appropriate attribute.. - /// - internal static string MissingAuditInformation { - get { - return ResourceManager.GetString("MissingAuditInformation", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The start parameter in multipart/related request is not supported.. - /// - internal static string StartParameterIsNotSupported { - get { - return ResourceManager.GetString("StartParameterIsNotSupported", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Date and time value must specify a time zone.. - /// - internal static string TimeZoneRequired { - get { - return ResourceManager.GetString("TimeZoneRequired", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The maximum number of custom audit headers allowed is {0}. The number of custom audit headers supplied is {1}.. - /// - internal static string TooManyCustomAuditHeaders { - get { - return ResourceManager.GetString("TooManyCustomAuditHeaders", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The specified content type '{0}' is not supported.. - /// - internal static string UnsupportedContentType { - get { - return ResourceManager.GetString("UnsupportedContentType", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The field is not supported: "{0}".. - /// - internal static string UnsupportedField { - get { - return ResourceManager.GetString("UnsupportedField", resourceCulture); - } - } - } -} diff --git a/src/Microsoft.Health.Dicom.Api/DicomApiResource.resx b/src/Microsoft.Health.Dicom.Api/DicomApiResource.resx deleted file mode 100644 index bbf91d490a..0000000000 --- a/src/Microsoft.Health.Dicom.Api/DicomApiResource.resx +++ /dev/null @@ -1,182 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - The maximum length of a custom audit header value is {0}. The supplied custom audit header '{1}' has length of {2}. - PH0 is a constant defining the max length of a header. PH1 is the name of the header sent in the request. PH2 is the length of the incoming header value. Example: The maximum length of a custom audit header value is 2048. The supplied custom audit header 'X-MS-AZUREFHIR-AUDIT-SITE' has length of 3072. - - - Invalid QIDO-RS query. Duplicate AttributeId '{0}'. Each attribute is only allowed to be specified once. - {0} specified attributeid - - - Cannot specify parameter more than once. - - - The server encountered an internal error. Please retry the request. If the issue persists, please contact support. - - - The value is not valid. It need to be one of "{0}". - {0} valid value set. - - - There was an error reading the multipart request. - - - The content type '{0}' is either not multipart or is missing boundary. - {0} is the specified content type. E.g., application/dicom - - - Unable to parse value '{0}' as {1}. - {0} is the value. {1} is the type. - - - The field '{0}' in request body is invalid: {1} - {0} is key, {1} is error message - - - The syntax of the request body is invalid. - - - The start time '{0:O}' must occur strictly before the end time '{1:O}'. - {0} is start. {1} is end. - - - The audit information is missing for Controller: {0} and Action: {1}. This usually means the action is not marked with appropriate attribute. - {0} is the controller name and {1} is the action name. - - - The start parameter in multipart/related request is not supported. - - - Date and time value must specify a time zone. - - - The maximum number of custom audit headers allowed is {0}. The number of custom audit headers supplied is {1}. - PH0 is a constant defining the max number of custom headers. PH1 is the count of custom headers sent in the request. Example: The maximum number of custom audit headers allowed is 10. The number of custom audit headers supplied is 12. - - - The specified content type '{0}' is not supported. - {0} is the specified content type. E.g., application/dicom - - - The field is not supported: "{0}". - {0} is field. - - \ No newline at end of file diff --git a/src/Microsoft.Health.Dicom.Api/Extensions/HttpContextExtensions.cs b/src/Microsoft.Health.Dicom.Api/Extensions/HttpContextExtensions.cs deleted file mode 100644 index 85da78d26c..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Extensions/HttpContextExtensions.cs +++ /dev/null @@ -1,27 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc.Versioning; - -namespace Microsoft.Health.Dicom.Api.Extensions; - -internal static class HttpContextExtensions -{ - public static int GetMajorRequestedApiVersion(this HttpContext context) - { - EnsureArg.IsNotNull(context, nameof(context)); - - var feature = context?.Features.Get(); - - if (feature?.RouteParameter != null) - { - return feature.RequestedApiVersion?.MajorVersion ?? 1; - } - - return 1; - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Extensions/HttpRequestExtensions.cs b/src/Microsoft.Health.Dicom.Api/Extensions/HttpRequestExtensions.cs deleted file mode 100644 index 0133b8efd6..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Extensions/HttpRequestExtensions.cs +++ /dev/null @@ -1,55 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using EnsureThat; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Primitives; -using Microsoft.Health.Dicom.Core.Messages.Retrieve; -using Microsoft.Health.Dicom.Core.Web; -using Microsoft.Net.Http.Headers; - -namespace Microsoft.Health.Dicom.Api.Extensions; - -public static class HttpRequestExtensions -{ - public static IReadOnlyCollection GetAcceptHeaders(this HttpRequest httpRequest) - { - EnsureArg.IsNotNull(httpRequest, nameof(httpRequest)); - IList acceptHeaders = httpRequest.GetTypedHeaders().Accept; - - return acceptHeaders?.Count > 0 - ? acceptHeaders.Select(item => item.ToAcceptHeader()).ToList() - : Array.Empty(); - } - - /// - /// Get host name from httpRequest - /// - /// The httpRequest. - /// True if follow dicom standards, false otherwise. - /// The host. - public static string GetHost(this HttpRequest httpRequest, bool dicomStandards = false) - { - EnsureArg.IsNotNull(httpRequest, nameof(httpRequest)); - string host = httpRequest.Host.Host; - if (dicomStandards && !string.IsNullOrWhiteSpace(host)) - { - // As Dicom standard, should append colon after service. https://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_11.7.3.2.html - host = host + ":"; - } - return host; - } - - public static bool IsOriginalVersionRequested(this HttpRequest httpRequest) - { - EnsureArg.IsNotNull(httpRequest, nameof(httpRequest)); - - return httpRequest.Headers.TryGetValue(OtherHeaderParameterNames.RequestOriginal, out StringValues stringValues) - && stringValues.Count > 0 && stringValues.First().Equals(bool.TrueString, StringComparison.OrdinalIgnoreCase); - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Extensions/HttpResponseExtensions.cs b/src/Microsoft.Health.Dicom.Api/Extensions/HttpResponseExtensions.cs deleted file mode 100644 index 1184d97ab6..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Extensions/HttpResponseExtensions.cs +++ /dev/null @@ -1,69 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Globalization; -using EnsureThat; -using Microsoft.AspNetCore.Http; -using Microsoft.Health.Dicom.Core.Models; -using Microsoft.Net.Http.Headers; - -namespace Microsoft.Health.Dicom.Api.Extensions; - -internal static class HttpResponseExtensions -{ - public const string ErroneousAttributesHeader = "erroneous-dicom-attributes"; - private const string WarningHeaderPattern = "{0} {1} \"{2}\""; - private const string UnknownAgentHost = "-"; - - // ExampleRoot is necessary as GetComponents, like many of the URI members, throws an exception - // when used on relative URI instances. As a workaround, we use it to help perform operations on relative URIs. - private static readonly Uri ExampleRoot = new Uri("https://example.com/", UriKind.Absolute); - - public static void AddLocationHeader(this HttpResponse response, Uri locationUrl) - { - EnsureArg.IsNotNull(response, nameof(response)); - EnsureArg.IsNotNull(locationUrl, nameof(locationUrl)); - - response.Headers.Append(HeaderNames.Location, locationUrl.IsAbsoluteUri ? locationUrl.AbsoluteUri : GetRelativeUri(locationUrl)); - } - - /// - /// Set Response Warning header. - /// - /// The httpResponse. - /// Warning code. - /// Host name. - /// The warning message. - public static void SetWarning(this HttpResponse response, HttpWarningCode code, string host, string message) - { - EnsureArg.IsNotNull(response, nameof(response)); - EnsureArg.IsNotEmptyOrWhiteSpace(message, nameof(message)); - - if (string.IsNullOrWhiteSpace(host)) - { - host = UnknownAgentHost; - } - - response.Headers.Warning = string.Format(CultureInfo.InvariantCulture, WarningHeaderPattern, (int)code, host, message); - } - - public static bool TryAddErroneousAttributesHeader(this HttpResponse response, IReadOnlyCollection erroneousAttributes) - { - EnsureArg.IsNotNull(response, nameof(response)); - EnsureArg.IsNotNull(erroneousAttributes, nameof(erroneousAttributes)); - if (erroneousAttributes.Count == 0) - { - return false; - } - - response.Headers.Append(ErroneousAttributesHeader, string.Join(",", erroneousAttributes)); - return true; - } - - private static string GetRelativeUri(Uri uri) - => new Uri(ExampleRoot, uri).GetComponents(UriComponents.AbsoluteUri & ~UriComponents.SchemeAndServer & ~UriComponents.UserInfo, UriFormat.UriEscaped); -} diff --git a/src/Microsoft.Health.Dicom.Api/Extensions/MediaTypeHeaderValueExtensions.cs b/src/Microsoft.Health.Dicom.Api/Extensions/MediaTypeHeaderValueExtensions.cs deleted file mode 100644 index 5e9f935e38..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Extensions/MediaTypeHeaderValueExtensions.cs +++ /dev/null @@ -1,56 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using EnsureThat; -using Microsoft.Extensions.Primitives; -using Microsoft.Health.Dicom.Core.Features.Retrieve; -using Microsoft.Health.Dicom.Core.Messages.Retrieve; -using Microsoft.Health.Dicom.Core.Web; -using Microsoft.Net.Http.Headers; - -namespace Microsoft.Health.Dicom.Api.Extensions; - -public static class MediaTypeHeaderValueExtensions -{ - public static StringSegment GetParameter(this MediaTypeHeaderValue headerValue, string parameterName, bool tryRemoveQuotes = true) - { - EnsureArg.IsNotNull(headerValue, nameof(headerValue)); - EnsureArg.IsNotEmptyOrWhiteSpace(parameterName, nameof(parameterName)); - foreach (NameValueHeaderValue parameter in headerValue.Parameters) - { - if (StringSegment.Equals(parameter.Name, parameterName, StringComparison.OrdinalIgnoreCase)) - { - return tryRemoveQuotes ? HeaderUtilities.RemoveQuotes(parameter.Value) : parameter.Value; - } - } - - return StringSegment.Empty; - } - - public static AcceptHeader ToAcceptHeader(this MediaTypeHeaderValue headerValue) - { - EnsureArg.IsNotNull(headerValue, nameof(headerValue)); - StringSegment mediaType = headerValue.MediaType; - - bool isMultipartRelated = StringSegment.Equals(KnownContentTypes.MultipartRelated, mediaType, StringComparison.OrdinalIgnoreCase); - // handle accept type with no quotes like "multipart/related; type=application/octet-stream; transfer-syntax=*" - // RFC 2045 is clear that any content type parameter value must be quoted if it contains at least one special character. - // However, RFC 2387 which defines `multipart/related` specifies in its ABNF definition of the `type` parameter that quotes are not allowed, although all examples include quotes (Errata 5048). - // The DICOMweb standard currently requires quotes, but will soon (CP 1776) allow both forms, so we will allow both. - bool? startsWithMultiPart = mediaType.Buffer?.StartsWith(KnownContentTypes.MultipartRelated, StringComparison.OrdinalIgnoreCase); - if (isMultipartRelated) - { - mediaType = headerValue.GetParameter(AcceptHeaderParameterNames.Type); - } - else if (startsWithMultiPart.HasValue && startsWithMultiPart == true) - { - isMultipartRelated = true; - } - - StringSegment transferSyntax = headerValue.GetParameter(AcceptHeaderParameterNames.TransferSyntax); - return new AcceptHeader(mediaType, isMultipartRelated ? PayloadTypes.MultipartRelated : PayloadTypes.SinglePart, transferSyntax, headerValue.Quality); - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Extensions/QueryOptionsExtensions.cs b/src/Microsoft.Health.Dicom.Api/Extensions/QueryOptionsExtensions.cs deleted file mode 100644 index 1d5bf14696..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Extensions/QueryOptionsExtensions.cs +++ /dev/null @@ -1,76 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Globalization; -using Microsoft.Extensions.Primitives; -using Microsoft.Health.Dicom.Api.Models; -using Microsoft.Health.Dicom.Core.Features.Query; - -namespace Microsoft.Health.Dicom.Api.Extensions; - -internal static class QueryOptionsExtensions -{ - private static readonly ImmutableHashSet KnownParameters = ImmutableHashSet.Create( - StringComparer.OrdinalIgnoreCase, - nameof(QueryOptions.FuzzyMatching), - nameof(QueryOptions.IncludeField), - nameof(QueryOptions.Limit), - nameof(QueryOptions.Offset)); - - public static QueryParameters ToQueryParameters( - this QueryOptions options, - IEnumerable> queryString, - QueryResource resourceType, - string studyInstanceUid = null, - string seriesInstanceUid = null) - { - var parameters = ToBaseQueryParameters(options, queryString); - return new QueryParameters - { - Filters = parameters.Filters, - FuzzyMatching = parameters.FuzzyMatching, - IncludeField = parameters.IncludeField, - Limit = parameters.Limit, - Offset = parameters.Offset, - QueryResourceType = resourceType, - SeriesInstanceUid = seriesInstanceUid, - StudyInstanceUid = studyInstanceUid, - }; - } - - public static BaseQueryParameters ToBaseQueryParameters( - this QueryOptions options, - IEnumerable> queryString) - { - // Parse the remaining query-string parameters into a dictionary - var filters = new Dictionary(); - foreach (KeyValuePair qsp in queryString) - { - string attributeId = qsp.Key.Trim(); - if (!KnownParameters.Contains(attributeId)) - { - if (qsp.Value.Count > 1) - { - throw new QueryParseException(string.Format(CultureInfo.CurrentCulture, DicomApiResource.DuplicateAttributeId, attributeId)); - } - - // No need to also check for duplicate keys as they are aggregated together in StringValues - filters.Add(attributeId, qsp.Value[0].Trim()); - } - } - - return new BaseQueryParameters - { - Filters = filters, - FuzzyMatching = options.FuzzyMatching, - IncludeField = options.IncludeField, - Limit = options.Limit, - Offset = options.Offset, - }; - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Extensions/StoreResponseStatusExtensions.cs b/src/Microsoft.Health.Dicom.Api/Extensions/StoreResponseStatusExtensions.cs deleted file mode 100644 index 7fc370b83a..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Extensions/StoreResponseStatusExtensions.cs +++ /dev/null @@ -1,32 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Net; -using Microsoft.Health.Dicom.Core.Messages.Store; - -namespace Microsoft.Health.Dicom.Api.Extensions; - -/// -/// Provides extension methods for . -/// -public static class StoreResponseStatusExtensions -{ - private static readonly IReadOnlyDictionary StoreResponseStatusToHttpStatusCodeMapping = new Dictionary() - { - { StoreResponseStatus.None, HttpStatusCode.NoContent }, - { StoreResponseStatus.Success, HttpStatusCode.OK }, - { StoreResponseStatus.PartialSuccess, HttpStatusCode.Accepted }, - { StoreResponseStatus.Failure, HttpStatusCode.Conflict }, - }; - - /// - /// Converts from to . - /// - /// The status to convert. - /// The converted . - public static HttpStatusCode ToHttpStatusCode(this StoreResponseStatus status) - => StoreResponseStatusToHttpStatusCodeMapping[status]; -} diff --git a/src/Microsoft.Health.Dicom.Api/Extensions/WorkitemResponseStatusExtensions.cs b/src/Microsoft.Health.Dicom.Api/Extensions/WorkitemResponseStatusExtensions.cs deleted file mode 100644 index da7d4b0b39..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Extensions/WorkitemResponseStatusExtensions.cs +++ /dev/null @@ -1,113 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Net; -using Microsoft.Health.Dicom.Core.Messages.Workitem; - -namespace Microsoft.Health.Dicom.Api.Extensions; - -public static class WorkitemResponseStatusExtensions -{ - private static readonly IReadOnlyDictionary AddResponseStatusToHttpStatusCodeMapping = - new Dictionary() - { - { WorkitemResponseStatus.None, HttpStatusCode.NoContent }, - { WorkitemResponseStatus.Success, HttpStatusCode.Created }, - { WorkitemResponseStatus.Failure, HttpStatusCode.BadRequest }, - { WorkitemResponseStatus.Conflict, HttpStatusCode.Conflict }, - }; - - private static readonly IReadOnlyDictionary CancelResponseStatusToHttpStatusCodeMapping = - new Dictionary() - { - { WorkitemResponseStatus.Success, HttpStatusCode.Accepted }, - { WorkitemResponseStatus.NotFound, HttpStatusCode.NotFound }, - { WorkitemResponseStatus.Failure, HttpStatusCode.BadRequest }, - { WorkitemResponseStatus.Conflict, HttpStatusCode.Conflict } - }; - - private static readonly IReadOnlyDictionary QueryResponseStatusToHttpStatusCodeMapping = - new Dictionary() - { - { WorkitemResponseStatus.Success, HttpStatusCode.OK }, - { WorkitemResponseStatus.NoContent, HttpStatusCode.NoContent }, - { WorkitemResponseStatus.PartialContent, HttpStatusCode.PartialContent }, - }; - - private static readonly IReadOnlyDictionary ChangeStateResponseStatusToHttpStatusCodeMapping = - new Dictionary() - { - { WorkitemResponseStatus.Success, HttpStatusCode.OK }, - { WorkitemResponseStatus.Failure, HttpStatusCode.BadRequest }, - { WorkitemResponseStatus.NotFound, HttpStatusCode.NotFound }, - { WorkitemResponseStatus.Conflict, HttpStatusCode.Conflict }, - }; - - private static readonly IReadOnlyDictionary RetrieveResponseStatusToHttpStatusCodeMapping = - new Dictionary() - { - { WorkitemResponseStatus.Success, HttpStatusCode.OK }, - { WorkitemResponseStatus.Failure, HttpStatusCode.BadRequest }, - { WorkitemResponseStatus.NotFound, HttpStatusCode.NotFound }, - }; - - private static readonly IReadOnlyDictionary UpdateResponseStatusToHttpStatusCodeMapping = - new Dictionary() - { - { WorkitemResponseStatus.Success, HttpStatusCode.OK }, - { WorkitemResponseStatus.Failure, HttpStatusCode.BadRequest }, - { WorkitemResponseStatus.NotFound, HttpStatusCode.NotFound }, - { WorkitemResponseStatus.Conflict, HttpStatusCode.Conflict }, - }; - - /// - /// Converts from to . - /// - /// The status to convert. - /// The converted . - public static HttpStatusCode AddResponseToHttpStatusCode(this WorkitemResponseStatus status) - => AddResponseStatusToHttpStatusCodeMapping[status]; - - /// - /// Converts from to . - /// - /// The status to convert. - /// The converted . - public static HttpStatusCode CancelResponseToHttpStatusCode(this WorkitemResponseStatus status) - => CancelResponseStatusToHttpStatusCodeMapping[status]; - - /// - /// Converts from to . - /// - /// The status to convert. - /// The converted . - public static HttpStatusCode QueryResponseToHttpStatusCode(this WorkitemResponseStatus status) - => QueryResponseStatusToHttpStatusCodeMapping[status]; - - /// - /// Converts from to . - /// - /// The status to convert. - /// The converted . - public static HttpStatusCode ChangeStateResponseToHttpStatusCode(this WorkitemResponseStatus status) - => ChangeStateResponseStatusToHttpStatusCodeMapping[status]; - - /// - /// Converts from to . - /// - /// The status to convert. - /// The converted . - public static HttpStatusCode RetrieveResponseToHttpStatusCode(this WorkitemResponseStatus status) - => RetrieveResponseStatusToHttpStatusCodeMapping[status]; - - /// - /// Converts from to . - /// - /// The status to convert. - /// The converted . - public static HttpStatusCode UpdateResponseToHttpStatusCode(this WorkitemResponseStatus status) - => UpdateResponseStatusToHttpStatusCodeMapping[status]; -} diff --git a/src/Microsoft.Health.Dicom.Api/Features/Audit/AuditHelper.cs b/src/Microsoft.Health.Dicom.Api/Features/Audit/AuditHelper.cs deleted file mode 100644 index 800b2f2fbc..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Features/Audit/AuditHelper.cs +++ /dev/null @@ -1,85 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Net; -using EnsureThat; -using Microsoft.AspNetCore.Http; -using Microsoft.Health.Api.Features.Audit; -using Microsoft.Health.Core.Features.Audit; -using Microsoft.Health.Core.Features.Context; -using Microsoft.Health.Core.Features.Security; -using Microsoft.Health.Dicom.Core.Features.Audit; -using Microsoft.Health.Dicom.Core.Features.Context; - -namespace Microsoft.Health.Dicom.Api.Features.Audit; - -public class AuditHelper : IAuditHelper -{ - private readonly IDicomRequestContextAccessor _dicomRequestContextAccessor; - private readonly IAuditLogger _auditLogger; - private readonly IAuditHeaderReader _auditHeaderReader; - - public AuditHelper( - IDicomRequestContextAccessor dicomRequestContextAccessor, - IAuditLogger auditLogger, - IAuditHeaderReader auditHeaderReader) - { - EnsureArg.IsNotNull(dicomRequestContextAccessor, nameof(dicomRequestContextAccessor)); - EnsureArg.IsNotNull(auditLogger, nameof(auditLogger)); - EnsureArg.IsNotNull(auditHeaderReader, nameof(auditHeaderReader)); - - _dicomRequestContextAccessor = dicomRequestContextAccessor; - _auditLogger = auditLogger; - _auditHeaderReader = auditHeaderReader; - } - - /// - public void LogExecuting(HttpContext httpContext, IClaimsExtractor claimsExtractor) - { - EnsureArg.IsNotNull(claimsExtractor, nameof(claimsExtractor)); - EnsureArg.IsNotNull(httpContext, nameof(httpContext)); - - Log(AuditAction.Executing, statusCode: null, httpContext, claimsExtractor); - } - - /// - /// Logs an executed audit entry for the current operation. - /// - /// The HTTP context. - /// The extractor used to extract claims. - /// - /// Should check for AuthX failure and print LogExecuted messages only if it is AuthX failure. - /// This is no-op in DICOM as all the log executed messages are written at one place. - /// - /// The duration of the operation in milliseconds. - public void LogExecuted(HttpContext httpContext, IClaimsExtractor claimsExtractor, bool shouldCheckForAuthXFailure, long? durationMs = null) - { - EnsureArg.IsNotNull(claimsExtractor, nameof(claimsExtractor)); - EnsureArg.IsNotNull(httpContext, nameof(httpContext)); - - Log(AuditAction.Executed, (HttpStatusCode)httpContext.Response.StatusCode, httpContext, claimsExtractor); - } - - private void Log(AuditAction auditAction, HttpStatusCode? statusCode, HttpContext httpContext, IClaimsExtractor claimsExtractor) - { - IRequestContext dicomRequestContext = _dicomRequestContextAccessor.RequestContext; - - string auditEventType = dicomRequestContext.AuditEventType; - - // Audit the call if an audit event type is associated with the action. - if (!string.IsNullOrEmpty(auditEventType)) - { - _auditLogger.LogAudit( - auditAction, - operation: auditEventType, - requestUri: dicomRequestContext.Uri, - statusCode: statusCode, - correlationId: dicomRequestContext.CorrelationId, - callerIpAddress: httpContext.Connection?.RemoteIpAddress?.ToString(), - callerClaims: claimsExtractor.Extract(), - customHeaders: _auditHeaderReader.Read(httpContext)); - } - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Features/Audit/AuditLoggingFilterAttribute.cs b/src/Microsoft.Health.Dicom.Api/Features/Audit/AuditLoggingFilterAttribute.cs deleted file mode 100644 index 294651cd39..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Features/Audit/AuditLoggingFilterAttribute.cs +++ /dev/null @@ -1,42 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Diagnostics.CodeAnalysis; -using EnsureThat; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.Health.Api.Features.Audit; -using Microsoft.Health.Core.Features.Security; - -namespace Microsoft.Health.Dicom.Api.Features.Audit; - -[AttributeUsage(AttributeTargets.Class)] -[SuppressMessage("Performance", "CA1813:Avoid unsealed attributes", Justification = "This attribute to meant to be extended.")] -public class AuditLoggingFilterAttribute : ActionFilterAttribute -{ - public AuditLoggingFilterAttribute( - IClaimsExtractor claimsExtractor, - IAuditHelper auditHelper) - { - EnsureArg.IsNotNull(claimsExtractor, nameof(claimsExtractor)); - EnsureArg.IsNotNull(auditHelper, nameof(auditHelper)); - - ClaimsExtractor = claimsExtractor; - AuditHelper = auditHelper; - } - - protected IClaimsExtractor ClaimsExtractor { get; } - - protected IAuditHelper AuditHelper { get; } - - public override void OnActionExecuting(ActionExecutingContext context) - { - EnsureArg.IsNotNull(context, nameof(context)); - - AuditHelper.LogExecuting(context.HttpContext, ClaimsExtractor); - - base.OnActionExecuting(context); - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Features/BackgroundServices/DeletedInstanceCleanupBackgroundService.cs b/src/Microsoft.Health.Dicom.Api/Features/BackgroundServices/DeletedInstanceCleanupBackgroundService.cs deleted file mode 100644 index 8974e8ddce..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Features/BackgroundServices/DeletedInstanceCleanupBackgroundService.cs +++ /dev/null @@ -1,35 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -namespace Microsoft.Health.Dicom.Api.Features.BackgroundServices; - -public class DeletedInstanceCleanupBackgroundService : BackgroundService -{ - private readonly IServiceProvider _serviceProvider; - - public DeletedInstanceCleanupBackgroundService(IServiceProvider serviceProvider) - { - EnsureArg.IsNotNull(serviceProvider, nameof(serviceProvider)); - - _serviceProvider = serviceProvider; - } - - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - using (var scope = _serviceProvider.CreateScope()) - { - var scopedDeletedInstanceCleanupWorker = scope.ServiceProvider.GetRequiredService(); - - await scopedDeletedInstanceCleanupWorker.ExecuteAsync(stoppingToken); - } - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Features/BackgroundServices/DeletedInstanceCleanupWorker.cs b/src/Microsoft.Health.Dicom.Api/Features/BackgroundServices/DeletedInstanceCleanupWorker.cs deleted file mode 100644 index e7e3cf351d..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Features/BackgroundServices/DeletedInstanceCleanupWorker.cs +++ /dev/null @@ -1,93 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Diagnostics.CodeAnalysis; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.Data.SqlClient; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Delete; -using Microsoft.Health.Dicom.Core.Features.Telemetry; -using Microsoft.Health.Dicom.Core.Models.Delete; -using Microsoft.Health.SqlServer.Features.Storage; - -namespace Microsoft.Health.Dicom.Api.Features.BackgroundServices; - -public class DeletedInstanceCleanupWorker -{ - private readonly ILogger _logger; - private readonly IDeleteService _deleteService; - private readonly DeleteMeter _deleteMeter; - private readonly TimeSpan _pollingInterval; - private readonly int _batchSize; - - public DeletedInstanceCleanupWorker( - IDeleteService deleteService, - DeleteMeter deleteMeter, - IOptions backgroundCleanupConfiguration, - ILogger logger) - { - EnsureArg.IsNotNull(deleteService, nameof(deleteService)); - EnsureArg.IsNotNull(deleteMeter, nameof(deleteMeter)); - EnsureArg.IsNotNull(backgroundCleanupConfiguration?.Value, nameof(backgroundCleanupConfiguration)); - EnsureArg.IsNotNull(logger, nameof(logger)); - - _deleteService = deleteService; - _deleteMeter = deleteMeter; - _pollingInterval = backgroundCleanupConfiguration.Value.PollingInterval; - _batchSize = backgroundCleanupConfiguration.Value.BatchSize; - _logger = logger; - } - - [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Do not throw exceptions other than those for cancellation.")] - public async Task ExecuteAsync(CancellationToken stoppingToken) - { - while (!stoppingToken.IsCancellationRequested) - { - try - { - await Task.Delay(_pollingInterval, stoppingToken).ConfigureAwait(false); - - // Send metrics related to deletion progress - DeleteMetrics metrics = await _deleteService.GetMetricsAsync(stoppingToken); - - _deleteMeter.OldestRequestedDeletion.Add(metrics.OldestDeletion.ToUnixTimeSeconds()); - _deleteMeter.CountDeletionsMaxRetry.Add(metrics.TotalExhaustedRetries); - - // Delete all instances pending deletion - bool success; - int retrievedInstanceCount; - do - { - (success, retrievedInstanceCount) = await _deleteService.CleanupDeletedInstancesAsync(stoppingToken); - } - while (success && retrievedInstanceCount == _batchSize); - } - catch (DataStoreNotReadyException) - { - _logger.LogInformation("The data store is not currently ready. Processing will continue after the next wait period."); - } - catch (SqlException sqlEx) when (sqlEx.IsCMKError()) - { - _logger.LogInformation(sqlEx, "The customer-managed key is misconfigured by the customer."); - } - catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) - { - // Cancel requested. - throw; - } - catch (Exception ex) - { - // The job failed. - _logger.LogCritical(ex, "Unhandled exception in the deleted instance cleanup worker."); - } - } - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Features/BackgroundServices/StartContentLengthBackFillBackgroundService.cs b/src/Microsoft.Health.Dicom.Api/Features/BackgroundServices/StartContentLengthBackFillBackgroundService.cs deleted file mode 100644 index 27e16cdeea..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Features/BackgroundServices/StartContentLengthBackFillBackgroundService.cs +++ /dev/null @@ -1,85 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Diagnostics.CodeAnalysis; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Features.Operations; -using Microsoft.Health.Dicom.Core.Models.Operations; -using Microsoft.Health.Operations; - -namespace Microsoft.Health.Dicom.Api.Features.BackgroundServices; - -public class StartContentLengthBackFillBackgroundService : BackgroundService -{ - private readonly IServiceProvider _serviceProvider; - private readonly ILogger _logger; - private readonly ContentLengthBackFillConfiguration _config; - - public StartContentLengthBackFillBackgroundService( - IServiceProvider serviceProvider, - IOptions options, - ILogger logger) - { - _serviceProvider = EnsureArg.IsNotNull(serviceProvider, nameof(serviceProvider)); - _logger = EnsureArg.IsNotNull(logger, nameof(logger)); - EnsureArg.IsNotNull(options, nameof(options)); - _config = EnsureArg.IsNotNull(options?.Value, nameof(options)); - } - - [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Do not throw exceptions.")] - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - try - { - using IServiceScope scope = _serviceProvider.CreateScope(); - - IDicomOperationsClient operationsClient = scope.ServiceProvider.GetRequiredService(); - - // Get existing operation status - OperationCheckpointState existingInstance = await operationsClient.GetLastCheckpointAsync(_config.OperationId, stoppingToken); - - if (existingInstance == null) - { - _logger.LogInformation("No existing content length backfill fixing operation."); - } - else - { - _logger.LogInformation("Existing content length backfill operation is in status: '{Status}'", existingInstance.Status); - } - - if (IsOperationInterruptedOrNull(existingInstance)) - { - await operationsClient.StartContentLengthBackFillOperationAsync( - _config.OperationId, - stoppingToken); - } - else if (existingInstance.Status == OperationStatus.Succeeded) - { - _logger.LogInformation("Content length backfill operation with ID '{InstanceId}' has already completed successfully.", _config.OperationId); - } - else - { - _logger.LogInformation("Content length backfill operation with ID '{InstanceId}' has already been started by another client.", _config.OperationId); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Unhandled exception while starting content length backfill operation."); - } - } - - private static bool IsOperationInterruptedOrNull(OperationCheckpointState operation) - { - return operation == null || operation.Status is OperationStatus.Canceled or OperationStatus.Failed; - } -} \ No newline at end of file diff --git a/src/Microsoft.Health.Dicom.Api/Features/Context/DicomRequestContextMiddleware.cs b/src/Microsoft.Health.Dicom.Api/Features/Context/DicomRequestContextMiddleware.cs deleted file mode 100644 index 43e57ee6e5..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Features/Context/DicomRequestContextMiddleware.cs +++ /dev/null @@ -1,55 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Extensions; -using Microsoft.Health.Dicom.Core.Features.Context; - -namespace Microsoft.Health.Dicom.Api.Features.Context; - -public class DicomRequestContextMiddleware -{ - private readonly RequestDelegate _next; - - public DicomRequestContextMiddleware(RequestDelegate next) - { - _next = EnsureArg.IsNotNull(next, nameof(next)); - } - - public async Task Invoke(HttpContext context, IDicomRequestContextAccessor dicomRequestContextAccessor) - { - EnsureArg.IsNotNull(context, nameof(context)); - EnsureArg.IsNotNull(dicomRequestContextAccessor, nameof(dicomRequestContextAccessor)); - HttpRequest request = context.Request; - - var baseUri = new Uri(UriHelper.BuildAbsolute( - request.Scheme, - request.Host, - request.PathBase)); - - var uri = new Uri(UriHelper.BuildAbsolute( - request.Scheme, - request.Host, - request.PathBase, - request.Path, - request.QueryString)); - - var dicomRequestContext = new DicomRequestContext( - method: request.Method, - uri, - baseUri, - correlationId: System.Diagnostics.Activity.Current?.RootId, - context.Request.Headers, - context.Response.Headers); - - dicomRequestContextAccessor.RequestContext = dicomRequestContext; - - // Call the next delegate/middleware in the pipeline - await _next(context); - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Features/Context/DicomRequestContextMiddlewareExtensions.cs b/src/Microsoft.Health.Dicom.Api/Features/Context/DicomRequestContextMiddlewareExtensions.cs deleted file mode 100644 index 1ee9c68e67..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Features/Context/DicomRequestContextMiddlewareExtensions.cs +++ /dev/null @@ -1,20 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using Microsoft.AspNetCore.Builder; - -namespace Microsoft.Health.Dicom.Api.Features.Context; - -public static class DicomRequestContextMiddlewareExtensions -{ - public static IApplicationBuilder UseDicomRequestContext( - this IApplicationBuilder builder) - { - EnsureArg.IsNotNull(builder, nameof(builder)); - - return builder.UseMiddleware(); - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Features/Conventions/ApiVersionsConvention.cs b/src/Microsoft.Health.Dicom.Api/Features/Conventions/ApiVersionsConvention.cs deleted file mode 100644 index 66871cd2a0..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Features/Conventions/ApiVersionsConvention.cs +++ /dev/null @@ -1,118 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Linq; -using EnsureThat; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ApplicationModels; -using Microsoft.AspNetCore.Mvc.Versioning.Conventions; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Configs; -using System.Diagnostics; -using System.Globalization; - -namespace Microsoft.Health.Dicom.Api.Features.Conventions; - -/// -/// Handles the API version logic for our controllers -/// All GAed versions are mentioned in _allSupportedVersions -/// Preview version is mentioned in _upcomingVersion -/// Latest GAed version is mentioned in _currentVersion -/// use IntroducedInApiVersionAttribute to add new functionality on latest version only -/// -internal class ApiVersionsConvention : IControllerConvention -{ - private static readonly IReadOnlyList AllSupportedVersions = new List() - { - // this will result in null minor instead of 0 minor. There is no constructor on ApiVersion that allows this directly - ApiVersion.Parse("1.0-prerelease"), - ApiVersion.Parse("1"), - ApiVersion.Parse("2"), - }; - - /// - /// Add upcoming API versions here so they can be used for private previews. - /// When upcomingVersion is ready for GA, move upcomingVersion to allSupportedVersion and remove from here. - /// - internal static IReadOnlyList UpcomingVersion = new List() { }; - - internal const int CurrentVersion = 2; - internal const int MinimumSupportedVersionForDicomUpdate = 2; - - private readonly bool _isLatestApiVersionEnabled; - - public ApiVersionsConvention(IOptions featureConfiguration) - { - EnsureArg.IsNotNull(featureConfiguration, nameof(featureConfiguration)); - _isLatestApiVersionEnabled = featureConfiguration.Value.EnableLatestApiVersion; - } - - public bool Apply(IControllerConventionBuilder controller, ControllerModel controllerModel) - { - EnsureArg.IsNotNull(controller, nameof(controller)); - EnsureArg.IsNotNull(controllerModel, nameof(controllerModel)); - - var controllerIntroducedInVersion = controllerModel.Attributes - .Where(a => a.GetType() == typeof(IntroducedInApiVersionAttribute)) - .Cast() - .Select(x => x.Version) - .SingleOrDefault(); - - IEnumerable versions = AllSupportedVersions; - if (controllerIntroducedInVersion != null) - { - versions = GetAllSupportedVersions(controllerIntroducedInVersion.Value, CurrentVersion); - } - // when upcomingVersion is ready for GA, move upcomingVersion to allSupportedVersion - versions = _isLatestApiVersionEnabled == true ? versions.Union(UpcomingVersion) : versions; - controller.HasApiVersions(versions); - - var inactiveActions = controllerModel.Actions.Where(x => !IsEnabled(x, versions)).ToList(); - foreach (ActionModel action in inactiveActions) - { - controllerModel.Actions.Remove(action); - } - - return true; - } - - private static List GetAllSupportedVersions(int start, int end) - { - if (start < 1) - { - Debug.Fail("startApiVersion must be more >= 1"); - } - if (end < start) - { - Debug.Fail("currentApiVersion must be >= startApiVersion"); - } - - return Enumerable - .Range(start, end - start + 1) - .Select(v => ApiVersion.Parse(v.ToString(CultureInfo.InvariantCulture))) - .ToList(); - } - - private static bool IsEnabled(ActionModel actionModel, IEnumerable enabled) - { - HashSet actionVersions = actionModel.Attributes - .Where(a => a is MapToApiVersionAttribute) - .Cast() - .SelectMany(x => x.Versions) - .ToHashSet(); - - if (actionVersions.Count == 0) - return true; - - foreach (ApiVersion version in enabled) - { - if (actionVersions.Contains(version)) - return true; - } - - return false; - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Features/Conventions/IntroducedInApiVersionAttribute.cs b/src/Microsoft.Health.Dicom.Api/Features/Conventions/IntroducedInApiVersionAttribute.cs deleted file mode 100644 index 2cb0c04384..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Features/Conventions/IntroducedInApiVersionAttribute.cs +++ /dev/null @@ -1,23 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; - -namespace Microsoft.Health.Dicom.Api.Features.Conventions; - -/// -/// Represents the starting API version the controller class was introduced. -/// Don't use any attribute if you want to introduce the controller in all available versions. -/// Did not use ApiVersionAttribute as base because it is a collection of versions and we need just one version here. -/// -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] -internal sealed class IntroducedInApiVersionAttribute : Attribute -{ - public int? Version { get; init; } - public IntroducedInApiVersionAttribute(int version) - { - Version = version; - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Features/Converter/EnumNameJsonConverter.cs b/src/Microsoft.Health.Dicom.Api/Features/Converter/EnumNameJsonConverter.cs deleted file mode 100644 index 689d58e042..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Features/Converter/EnumNameJsonConverter.cs +++ /dev/null @@ -1,43 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Globalization; -using System.Text.Json; -using System.Text.Json.Serialization; -using EnsureThat; - -namespace Microsoft.Health.Dicom.Api.Features.Converter; - -/// -/// Enum JsonConverter to provide better error message to customer. -/// -/// Enum type. -public sealed class EnumNameJsonConverter : JsonConverter - where T : struct, Enum -{ - public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - EnsureArg.IsNotNull(typeToConvert, nameof(typeToConvert)); - - if (reader.TokenType == JsonTokenType.String) - { - string content = reader.GetString(); - if (Enum.TryParse(content, true, out T result)) - { - return result; - } - } - - throw new JsonException( - string.Format(CultureInfo.InvariantCulture, DicomApiResource.InvalidEnumValue, string.Join("\",\"", Enum.GetNames(typeToConvert)))); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - EnsureArg.IsNotNull(writer); - writer.WriteStringValue(Enum.GetName(value)); - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Features/Exceptions/ExceptionHandlingMiddleware.cs b/src/Microsoft.Health.Dicom.Api/Features/Exceptions/ExceptionHandlingMiddleware.cs deleted file mode 100644 index 20059474e5..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Features/Exceptions/ExceptionHandlingMiddleware.cs +++ /dev/null @@ -1,191 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.IO; -using System.Linq; -using System.Net; -using System.Runtime.ExceptionServices; -using System.Text.Json; -using System.Threading.Tasks; -using Azure; -using EnsureThat; -using Microsoft.AspNetCore.Connections; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Data.SqlClient; -using Microsoft.Extensions.Logging; -using Microsoft.Health.Abstractions.Exceptions; -using Microsoft.Health.Api.Features.Audit; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Encryption.Customer.Extensions; -using Microsoft.Health.SqlServer.Features.Storage; -using ComponentModelValidationException = System.ComponentModel.DataAnnotations.ValidationException; -using NotSupportedException = Microsoft.Health.Dicom.Core.Exceptions.NotSupportedException; - -namespace Microsoft.Health.Dicom.Api.Features.Exceptions; - -public class ExceptionHandlingMiddleware -{ - private readonly RequestDelegate _next; - private readonly ILogger _logger; - - public ExceptionHandlingMiddleware(RequestDelegate next, ILogger logger) - { - EnsureArg.IsNotNull(next, nameof(next)); - EnsureArg.IsNotNull(logger, nameof(logger)); - - _next = next; - _logger = logger; - } - - public async Task Invoke(HttpContext context) - { - EnsureArg.IsNotNull(context, nameof(context)); - ExceptionDispatchInfo exceptionDispatchInfo = null; - try - { - await _next(context); - } - catch (Exception exception) - { - if (context.Response.HasStarted) - { - _logger.LogWarning("The response has already started, the base exception middleware will not be executed."); - throw; - } - - // Get the Exception, but don't continue processing in the catch block as its bad for stack usage. - exceptionDispatchInfo = ExceptionDispatchInfo.Capture(exception); - } - - if (exceptionDispatchInfo != null) - { - IActionResult result = MapExceptionToResult(exceptionDispatchInfo.SourceException); - await ExecuteResultAsync(context, result); - } - } - - private ContentResult MapExceptionToResult(Exception exception) - { - HttpStatusCode statusCode = HttpStatusCode.InternalServerError; - string message = exception.Message; - - switch (exception) - { - case JsonException: - _logger.LogError(exception, nameof(JsonException)); - message = DicomApiResource.InvalidSyntax; - statusCode = HttpStatusCode.BadRequest; - break; - case ArgumentException: - case FormatException: - case InvalidOperationException: - case ValidationException: - case ComponentModelValidationException: - case NotSupportedException: - case AuditHeaderCountExceededException: - case AuditHeaderTooLargeException: - case ConnectionResetException: - case OperationCanceledException: - case MicrosoftHealthException ex when IsOperationCanceledException(ex.InnerException): - case BadHttpRequestException: - case IOException io when io.Message.Equals("The request stream was aborted.", StringComparison.OrdinalIgnoreCase): - statusCode = HttpStatusCode.BadRequest; - break; - case ConditionalExternalException ex when ex.IsExternal == true: - case ConditionalExternalException cee when IsCMKException(cee.InnerException): - case Exception e when IsCMKException(e): - statusCode = HttpStatusCode.FailedDependency; - break; - case ResourceNotFoundException: - statusCode = HttpStatusCode.NotFound; - break; - case NotAcceptableException: - case TranscodingException: - statusCode = HttpStatusCode.NotAcceptable; - break; - case DicomImageException: - statusCode = HttpStatusCode.NotAcceptable; - break; - case DataStoreException: - statusCode = HttpStatusCode.ServiceUnavailable; - break; - case InstanceAlreadyExistsException: - case ExtendedQueryTagsAlreadyExistsException: - case ExtendedQueryTagsOutOfDateException: - case ExistingOperationException: - statusCode = HttpStatusCode.Conflict; - break; - case PayloadTooLargeException: - statusCode = HttpStatusCode.RequestEntityTooLarge; - break; - case UnsupportedMediaTypeException: - statusCode = HttpStatusCode.UnsupportedMediaType; - break; - case ServiceUnavailableException: - statusCode = HttpStatusCode.ServiceUnavailable; - break; - case ItemNotFoundException: - // One of the required resources is missing. - statusCode = HttpStatusCode.InternalServerError; - break; - case UnauthorizedDicomActionException udae: - _logger.LogInformation("Expected data actions not available: {DataActions}", udae.ExpectedDataActions); - statusCode = HttpStatusCode.Forbidden; - break; - case DicomServerException: - statusCode = HttpStatusCode.ServiceUnavailable; - break; - } - - // Log the exception and possibly modify the user message - switch (statusCode) - { - case HttpStatusCode.ServiceUnavailable: - _logger.LogWarning(exception, "Service exception."); - break; - case HttpStatusCode.InternalServerError: - // In the case of InternalServerError, make sure to overwrite the message to - // avoid internal message. - _logger.LogCritical(exception, "Unexpected service exception."); - message = DicomApiResource.InternalServerError; - break; - default: - _logger.LogWarning(exception, "Unhandled exception"); - break; - } - - return GetContentResult(statusCode, message); - } - - private static bool IsOperationCanceledException(Exception ex) - { - return ex is OperationCanceledException || (ex is AggregateException aggEx && aggEx.InnerExceptions.Any(x => x is OperationCanceledException)); - } - - private static bool IsCMKException(Exception ex) - { - return ex is SqlException sqlEx && sqlEx.IsCMKError() || - ex is RequestFailedException rfEx && rfEx.IsCMKError() || - (ex is AggregateException aggEx && aggEx.InnerExceptions.Any(x => x is SqlException sqlEx && sqlEx.IsCMKError() || x is RequestFailedException rfEx && rfEx.IsCMKError())); - } - - private static ContentResult GetContentResult(HttpStatusCode statusCode, string message) - { - return new ContentResult - { - StatusCode = (int)statusCode, - Content = message, - }; - } - - protected internal virtual async Task ExecuteResultAsync(HttpContext context, IActionResult result) - { - EnsureArg.IsNotNull(context, nameof(context)); - EnsureArg.IsNotNull(result, nameof(result)); - await result.ExecuteResultAsync(new ActionContext { HttpContext = context }); - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Features/Exceptions/ExceptionHandlingMiddlewareExtensions.cs b/src/Microsoft.Health.Dicom.Api/Features/Exceptions/ExceptionHandlingMiddlewareExtensions.cs deleted file mode 100644 index 2cd6f6c812..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Features/Exceptions/ExceptionHandlingMiddlewareExtensions.cs +++ /dev/null @@ -1,18 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using Microsoft.Health.Dicom.Api.Features.Exceptions; - -namespace Microsoft.AspNetCore.Builder; - -public static class ExceptionHandlingMiddlewareExtensions -{ - public static IApplicationBuilder UseExceptionHandling(this IApplicationBuilder builder) - { - EnsureArg.IsNotNull(builder); - return builder.UseMiddleware(); - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Features/Filters/AcceptContentFilterAttribute.cs b/src/Microsoft.Health.Dicom.Api/Features/Filters/AcceptContentFilterAttribute.cs deleted file mode 100644 index 66b27d765d..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Features/Filters/AcceptContentFilterAttribute.cs +++ /dev/null @@ -1,86 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Net; -using EnsureThat; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.Extensions.Primitives; -using Microsoft.Net.Http.Headers; - -namespace Microsoft.Health.Dicom.Api.Features.Filters; - -public sealed class AcceptContentFilterAttribute : ActionFilterAttribute -{ - private const int NotAcceptableResponseCode = (int)HttpStatusCode.NotAcceptable; - - private readonly HashSet _mediaTypes; - - public AcceptContentFilterAttribute(string[] mediaTypes) - { - EnsureArg.IsNotNull(mediaTypes, nameof(mediaTypes)); - Debug.Assert(mediaTypes.Length > 0, "The accept content type filter must have at least one media type specified."); - - _mediaTypes = new HashSet(mediaTypes.Length); - - foreach (var mediaType in mediaTypes) - { - if (MediaTypeHeaderValue.TryParse(mediaType, out MediaTypeHeaderValue parsedMediaType)) - { - _mediaTypes.Add(parsedMediaType); - } - else - { - Debug.Assert(false, "The values in the mediaTypes parameter must be parseable by MediaTypeHeaderValue."); - } - } - } - - public override void OnActionExecuting(ActionExecutingContext context) - { - EnsureArg.IsNotNull(context, nameof(context)); - - bool acceptable = AcceptHeadersContainKnownTypes(context.HttpContext.Request); - - if (!acceptable) - { - context.Result = new StatusCodeResult(NotAcceptableResponseCode); - } - - base.OnActionExecuting(context); - } - - private bool AcceptHeadersContainKnownTypes(HttpRequest request) - { - IList acceptHeaders = ParseAcceptHeaders(request.Headers.Accept); - - foreach (MediaTypeHeaderValue acceptHeader in acceptHeaders) - { - if (_mediaTypes.Any(x => acceptHeader.MatchesMediaType(x.MediaType))) - { - return true; - } - } - - return false; - } - - private static IList ParseAcceptHeaders(StringValues acceptHeaders) - { - try - { - return MediaTypeHeaderValue.ParseStrictList(acceptHeaders); - } - catch (FormatException) - { - return new List(); - } - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Features/Filters/AcceptTransferSyntaxFilterAttribute.cs b/src/Microsoft.Health.Dicom.Api/Features/Filters/AcceptTransferSyntaxFilterAttribute.cs deleted file mode 100644 index a1a81e256b..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Features/Filters/AcceptTransferSyntaxFilterAttribute.cs +++ /dev/null @@ -1,62 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Net; -using EnsureThat; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.Mvc.ModelBinding; - -namespace Microsoft.Health.Dicom.Api.Features.Filters; - -public sealed class AcceptTransferSyntaxFilterAttribute : ActionFilterAttribute -{ - private const int NotAcceptableResponseCode = (int)HttpStatusCode.NotAcceptable; - private const string TransferSyntaxHeaderPrefix = "transfer-syntax"; - private readonly bool _allowMissing; - private readonly HashSet _transferSyntaxes; - - public AcceptTransferSyntaxFilterAttribute(string[] transferSyntaxes, bool allowMissing = false) - { - EnsureArg.IsNotNull(transferSyntaxes, nameof(transferSyntaxes)); - Debug.Assert(transferSyntaxes.Length > 0, "The accept transfer syntax filter must have at least one transfer syntax specified."); - _transferSyntaxes = new HashSet(transferSyntaxes, StringComparer.InvariantCultureIgnoreCase); - _allowMissing = allowMissing; - } - - public override void OnActionExecuting(ActionExecutingContext context) - { - EnsureArg.IsNotNull(context, nameof(context)); - bool acceptable; - - // As model binding happens prior to filteration, use the transfer syntax that was found in TransferSyntaxModelBinder and validate if it is acceptable. - if (context.ModelState.TryGetValue(TransferSyntaxHeaderPrefix, out ModelStateEntry transferSyntaxValue)) - { - if (_transferSyntaxes.Contains(transferSyntaxValue.RawValue)) - { - acceptable = true; - } - else - { - acceptable = _allowMissing && string.IsNullOrWhiteSpace($"{transferSyntaxValue.RawValue}"); - } - } - else - { - acceptable = _allowMissing; - } - - if (!acceptable) - { - context.Result = new StatusCodeResult(NotAcceptableResponseCode); - } - - base.OnActionExecuting(context); - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Features/Filters/BodyModelStateValidatorAttribute.cs b/src/Microsoft.Health.Dicom.Api/Features/Filters/BodyModelStateValidatorAttribute.cs deleted file mode 100644 index 7f4d66a2fb..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Features/Filters/BodyModelStateValidatorAttribute.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Linq; -using EnsureThat; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.Health.Dicom.Api.Web; - -namespace Microsoft.Health.Dicom.Api.Features.Filters; - -public sealed class BodyModelStateValidatorAttribute : ActionFilterAttribute -{ - public override void OnActionExecuting(ActionExecutingContext context) - { - EnsureArg.IsNotNull(context, nameof(context)); - if (!context.ModelState.IsValid) - { - var error = context.ModelState.Last(x => x.Value.ValidationState == AspNetCore.Mvc.ModelBinding.ModelValidationState.Invalid); - throw new InvalidRequestBodyException(error.Key, error.Value.Errors.First().ErrorMessage); - } - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Features/Filters/DicomRequestContextRouteDataPopulatingFilterAttribute.cs b/src/Microsoft.Health.Dicom.Api/Features/Filters/DicomRequestContextRouteDataPopulatingFilterAttribute.cs deleted file mode 100644 index 04a00d5fd7..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Features/Filters/DicomRequestContextRouteDataPopulatingFilterAttribute.cs +++ /dev/null @@ -1,76 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using EnsureThat; -using Microsoft.AspNetCore.Mvc.Controllers; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.Routing; -using Microsoft.Health.Api.Features.Audit; -using Microsoft.Health.Dicom.Api.Extensions; -using Microsoft.Health.Dicom.Api.Features.Routing; -using Microsoft.Health.Dicom.Core.Features.Context; - -namespace Microsoft.Health.Dicom.Api.Features.Filters; - -[AttributeUsage(AttributeTargets.Class)] -public sealed class DicomRequestContextRouteDataPopulatingFilterAttribute : ActionFilterAttribute -{ - private readonly IDicomRequestContextAccessor _dicomRequestContextAccessor; - private readonly IAuditEventTypeMapping _auditEventTypeMapping; - - public DicomRequestContextRouteDataPopulatingFilterAttribute( - IDicomRequestContextAccessor dicomRequestContextAccessor, - IAuditEventTypeMapping auditEventTypeMapping) - { - EnsureArg.IsNotNull(dicomRequestContextAccessor, nameof(dicomRequestContextAccessor)); - EnsureArg.IsNotNull(auditEventTypeMapping, nameof(auditEventTypeMapping)); - - _dicomRequestContextAccessor = dicomRequestContextAccessor; - _auditEventTypeMapping = auditEventTypeMapping; - } - - public override void OnActionExecuting(ActionExecutingContext context) - { - EnsureArg.IsNotNull(context, nameof(context)); - - IDicomRequestContext dicomRequestContext = _dicomRequestContextAccessor.RequestContext; - dicomRequestContext.RouteName = context.ActionDescriptor?.AttributeRouteInfo?.Name; - dicomRequestContext.Version = context.HttpContext.GetMajorRequestedApiVersion(); - - // Set StudyInstanceUid, SeriesInstanceUid, and SopInstanceUid based on the route data - RouteData routeData = context.RouteData; - - if (routeData?.Values != null) - { - // Try to get StudyInstanceUid - if (routeData.Values.TryGetValue(KnownActionParameterNames.StudyInstanceUid, out object studyInstanceUid)) - { - dicomRequestContext.StudyInstanceUid = studyInstanceUid.ToString(); - - // Try to get SeriesInstanceUid only if StudyInstanceUid was successfully fetched. - if (routeData.Values.TryGetValue(KnownActionParameterNames.SeriesInstanceUid, out object seriesInstanceUid)) - { - dicomRequestContext.SeriesInstanceUid = seriesInstanceUid.ToString(); - - // Try to get SopInstanceUid only if StudyInstanceUid and SeriesInstanceUid were fetched successfully. - if (routeData.Values.TryGetValue(KnownActionParameterNames.SopInstanceUid, out object sopInstanceUid)) - { - dicomRequestContext.SopInstanceUid = sopInstanceUid.ToString(); - } - } - } - } - - if (context.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor) - { - dicomRequestContext.AuditEventType = _auditEventTypeMapping.GetAuditEventType( - controllerActionDescriptor.ControllerName, - controllerActionDescriptor.ActionName); - } - - base.OnActionExecuting(context); - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Features/Filters/PopulateDataPartitionFilterAttribute.cs b/src/Microsoft.Health.Dicom.Api/Features/Filters/PopulateDataPartitionFilterAttribute.cs deleted file mode 100644 index d232286ec8..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Features/Filters/PopulateDataPartitionFilterAttribute.cs +++ /dev/null @@ -1,83 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using EnsureThat; -using MediatR; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Api.Features.Routing; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Context; -using Microsoft.Health.Dicom.Core.Features.Partitioning; - -namespace Microsoft.Health.Dicom.Api.Features.Filters; - -[AttributeUsage(AttributeTargets.Class)] -public sealed class PopulateDataPartitionFilterAttribute : ActionFilterAttribute -{ - private readonly IDicomRequestContextAccessor _dicomRequestContextAccessor; - private readonly IMediator _mediator; - private readonly bool _isPartitionEnabled; - - private readonly HashSet _partitionCreationSupportedRouteNames = new HashSet - { - KnownRouteNames.PartitionStoreInstance, - KnownRouteNames.PartitionStoreInstancesInStudy, - KnownRouteNames.PartitionedAddWorkitemInstance, - }; - - public PopulateDataPartitionFilterAttribute( - IDicomRequestContextAccessor dicomRequestContextAccessor, - IMediator mediator, - IOptions featureConfiguration) - { - _dicomRequestContextAccessor = EnsureArg.IsNotNull(dicomRequestContextAccessor, nameof(dicomRequestContextAccessor)); - _mediator = EnsureArg.IsNotNull(mediator, nameof(mediator)); - - EnsureArg.IsNotNull(featureConfiguration, nameof(featureConfiguration)); - _isPartitionEnabled = featureConfiguration.Value.EnableDataPartitions; - } - - public async override Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) - { - EnsureArg.IsNotNull(context, nameof(context)); - IDicomRequestContext dicomRequestContext = _dicomRequestContextAccessor.RequestContext; - RouteData routeData = context.RouteData; - var routeName = context.ActionDescriptor?.AttributeRouteInfo?.Name; - - var routeContainsPartition = routeData.Values.TryGetValue(KnownActionParameterNames.PartitionName, out object value); - - if (!_isPartitionEnabled && routeContainsPartition) - throw new DataPartitionsFeatureDisabledException(); - - if (_isPartitionEnabled && !routeContainsPartition) - throw new DataPartitionsMissingPartitionException(); - - if (_isPartitionEnabled) - { - string partitionName = value?.ToString(); - - Partition partition; - if (_partitionCreationSupportedRouteNames.Contains(routeName)) - { - partition = (await _mediator.GetOrAddPartitionAsync(partitionName)).Partition; - } - else - { - partition = (await _mediator.GetPartitionAsync(partitionName)).Partition; - } - - dicomRequestContext.DataPartition = partition; - } - - await base.OnActionExecutionAsync(context, next); - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Features/Filters/QueryModelStateValidatorAttribute.cs b/src/Microsoft.Health.Dicom.Api/Features/Filters/QueryModelStateValidatorAttribute.cs deleted file mode 100644 index 97c5c98605..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Features/Filters/QueryModelStateValidatorAttribute.cs +++ /dev/null @@ -1,36 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Linq; -using System.Text.RegularExpressions; -using System.Web; -using EnsureThat; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.Health.Dicom.Core.Exceptions; - -namespace Microsoft.Health.Dicom.Api.Features.Filters; - -public sealed class QueryModelStateValidatorAttribute : ActionFilterAttribute -{ - private static readonly Regex HtmlCharacters = new Regex("<[^>]*>", RegexOptions.Compiled); - - public override void OnActionExecuting(ActionExecutingContext context) - { - EnsureArg.IsNotNull(context, nameof(context)); - if (!context.ModelState.IsValid) - { - (string key, ModelStateEntry value) = context.ModelState.Where(x => x.Value.Errors.Count > 0).First(); - - string errorMessage = value.Errors[0].ErrorMessage; - if (!string.IsNullOrEmpty(errorMessage) && HtmlCharacters.IsMatch(errorMessage)) - { - errorMessage = HttpUtility.HtmlEncode(errorMessage); - } - - throw new InvalidQueryStringValuesException(key, errorMessage); - } - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Features/ModelBinders/AggregateCsvModelBinder.cs b/src/Microsoft.Health.Dicom.Api/Features/ModelBinders/AggregateCsvModelBinder.cs deleted file mode 100644 index 1494bb2803..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Features/ModelBinders/AggregateCsvModelBinder.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.Extensions.Primitives; - -namespace Microsoft.Health.Dicom.Api.Features.ModelBinders; - -internal class AggregateCsvModelBinder : IModelBinder -{ - public Task BindModelAsync(ModelBindingContext bindingContext) - { - EnsureArg.IsNotNull(bindingContext, nameof(bindingContext)); - StringValues values = bindingContext.ValueProvider.GetValue(bindingContext.ModelName).Values; - - IReadOnlyList result = values.Count == 0 - ? Array.Empty() - : values.SelectMany(x => x.Split(',', StringSplitOptions.TrimEntries)).ToList(); - - bindingContext.Result = ModelBindingResult.Success(result); - return Task.CompletedTask; - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Features/ModelBinders/CsvModelBinder.T.cs b/src/Microsoft.Health.Dicom.Api/Features/ModelBinders/CsvModelBinder.T.cs deleted file mode 100644 index 407b43208d..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Features/ModelBinders/CsvModelBinder.T.cs +++ /dev/null @@ -1,56 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Globalization; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.Extensions.Primitives; - -namespace Microsoft.Health.Dicom.Api.Features.ModelBinders; - -internal abstract class CsvModelBinder : IModelBinder -{ - public Task BindModelAsync(ModelBindingContext bindingContext) - { - EnsureArg.IsNotNull(bindingContext, nameof(bindingContext)); - StringValues values = bindingContext.ValueProvider.GetValue(bindingContext.ModelName).Values; - - if (values.Count == 0) - { - bindingContext.Result = ModelBindingResult.Success(Array.Empty()); - } - else if (values.Count > 1) - { - bindingContext.ModelState.TryAddModelError(bindingContext.ModelName, DicomApiResource.DuplicateParameter); - } - else if (string.IsNullOrEmpty(values[0])) - { - bindingContext.Result = ModelBindingResult.Success(Array.Empty()); - } - else - { - string[] split = values[0].Split(',', StringSplitOptions.TrimEntries); - T[] parsed = new T[split.Length]; - for (int i = 0; i < split.Length; i++) - { - if (!TryParse(split[i], out T parsedValue)) - { - bindingContext.ModelState.TryAddModelError(bindingContext.ModelName, string.Format(CultureInfo.CurrentCulture, DicomApiResource.InvalidParse, split[i], typeof(T).Name)); - return Task.CompletedTask; - } - - parsed[i] = parsedValue; - } - - bindingContext.Result = ModelBindingResult.Success(parsed); - } - - return Task.CompletedTask; - } - - protected abstract bool TryParse(string value, out T result); -} diff --git a/src/Microsoft.Health.Dicom.Api/Features/ModelBinders/IntArrayModelBinder.cs b/src/Microsoft.Health.Dicom.Api/Features/ModelBinders/IntArrayModelBinder.cs deleted file mode 100644 index 7f62436233..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Features/ModelBinders/IntArrayModelBinder.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Globalization; - -namespace Microsoft.Health.Dicom.Api.Features.ModelBinders; - -internal class IntArrayModelBinder : CsvModelBinder -{ - protected override bool TryParse(string value, out int result) - => int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out result); -} diff --git a/src/Microsoft.Health.Dicom.Api/Features/ModelBinders/MandatoryTimeZoneBinder.cs b/src/Microsoft.Health.Dicom.Api/Features/ModelBinders/MandatoryTimeZoneBinder.cs deleted file mode 100644 index d59c39370a..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Features/ModelBinders/MandatoryTimeZoneBinder.cs +++ /dev/null @@ -1,50 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Globalization; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc.ModelBinding; - -namespace Microsoft.Health.Dicom.Api.Features.ModelBinders; - -internal class MandatoryTimeZoneBinder : IModelBinder -{ - public Task BindModelAsync(ModelBindingContext bindingContext) - { - if (bindingContext == null) - throw new ArgumentNullException(nameof(bindingContext)); - - string modelName = bindingContext.ModelName; - ValueProviderResult result = bindingContext.ValueProvider.GetValue(modelName); - if (result != ValueProviderResult.None) - { - bindingContext.ModelState.SetModelValue(modelName, result); - - string value = result.FirstValue; - if (!string.IsNullOrWhiteSpace(value)) - { - try - { - // Attempt to parse the string as a DateTime and convert it to UTC. - // The only reason DateTime is used is because the Kind property allows us to - // correctly determine whether time zone was specified. Otherwise, we'd have to - // construct a series of complicated regular expressions. - var dt = DateTime.Parse(value, CultureInfo.InvariantCulture, DateTimeStyles.None); - if (dt.Kind == DateTimeKind.Unspecified) - bindingContext.ModelState.TryAddModelError(modelName, DicomApiResource.TimeZoneRequired); - else - bindingContext.Result = ModelBindingResult.Success(new DateTimeOffset(dt.ToUniversalTime())); - } - catch (FormatException e) - { - bindingContext.ModelState.TryAddModelException(modelName, e); - } - } - } - - return Task.CompletedTask; - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Features/ModelBinders/TransferSyntaxModelBinder.cs b/src/Microsoft.Health.Dicom.Api/Features/ModelBinders/TransferSyntaxModelBinder.cs deleted file mode 100644 index 9f1af9491a..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Features/ModelBinders/TransferSyntaxModelBinder.cs +++ /dev/null @@ -1,56 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.Extensions.Primitives; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Net.Http.Headers; - -namespace Microsoft.Health.Dicom.Api.Features.ModelBinders; - -public class TransferSyntaxModelBinder : IModelBinder -{ - private const string TransferSyntaxHeaderPrefix = "transfer-syntax"; - - public Task BindModelAsync(ModelBindingContext bindingContext) - { - EnsureArg.IsNotNull(bindingContext, nameof(bindingContext)); - IList acceptHeaders = bindingContext.HttpContext.Request.GetTypedHeaders().Accept; - - // Validate the accept headers has one of the specified accepted media types. - if (acceptHeaders != null && acceptHeaders.Count > 0) - { - foreach (MediaTypeHeaderValue acceptHeader in acceptHeaders) - { - List typeParameterValue = acceptHeader.Parameters.Where( - parameter => StringSegment.Equals(parameter.Name, TransferSyntaxHeaderPrefix, StringComparison.InvariantCultureIgnoreCase)).ToList(); - - if (typeParameterValue.Count > 1) - { - throw new BadRequestException("Transfer Syntax parameter is specified more than once"); - } - - if (typeParameterValue != null && typeParameterValue.Count == 1) - { - StringSegment parsedValue = HeaderUtilities.RemoveQuotes(typeParameterValue.First().Value); - - ValueProviderResult valueProviderResult = new ValueProviderResult(parsedValue.ToString()); - bindingContext.ModelState.SetModelValue(TransferSyntaxHeaderPrefix, valueProviderResult); - bindingContext.Result = ModelBindingResult.Success(parsedValue.ToString()); - return Task.CompletedTask; - } - } - } - - bindingContext.Result = ModelBindingResult.Failed(); - return Task.CompletedTask; - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Features/Partitioning/DataPartitionFeatureValidatorService.cs b/src/Microsoft.Health.Dicom.Api/Features/Partitioning/DataPartitionFeatureValidatorService.cs deleted file mode 100644 index 0557033c7c..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Features/Partitioning/DataPartitionFeatureValidatorService.cs +++ /dev/null @@ -1,73 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Partitioning; - -namespace Microsoft.Health.Dicom.Api.Features.Partitioning; - -/// -/// This hosted service performs the check at startup to ensure user cannot disable DataPartitions feature flag -/// if they already created partitioned data other than the default partition. -/// If a user created partitioned data other than the default partition, then an exception is thrown to block startup. -/// We will allow users to enable DataPartitions feature even if they have already created data, -/// it will be accessible by specifying Microsoft.Default partition name in the request. -/// -public class DataPartitionFeatureValidatorService : IHostedService -{ - private readonly IServiceProvider _serviceProvider; - private readonly bool _isPartitionEnabled; - private readonly ILogger _logger; - - public DataPartitionFeatureValidatorService( - IServiceProvider serviceProvider, - IOptions featureConfiguration, - ILogger logger) - { - _serviceProvider = serviceProvider; - EnsureArg.IsNotNull(featureConfiguration?.Value, nameof(featureConfiguration)); - - _logger = EnsureArg.IsNotNull(logger, nameof(logger)); - - _isPartitionEnabled = featureConfiguration.Value.EnableDataPartitions; - } - - public async Task StartAsync(CancellationToken cancellationToken) - { - if (!_isPartitionEnabled) - { - try - { - using (var scope = _serviceProvider.CreateScope()) - { - var partitionService = scope.ServiceProvider.GetRequiredService(); - - var partitions = await partitionService.GetPartitionsAsync(cancellationToken); - - if (partitions.Entries.Count > 1) - { - throw new DataPartitionsFeatureCannotBeDisabledException(); - } - } - } - catch (DataStoreNotReadyException ex) - { - // If a consumer doesn't upgrade the schema, then the service won't be started. So silently failing. - _logger.LogWarning("Silently failing, schema version not upgraded. {Message}", ex.Message); - } - } - } - - public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; -} diff --git a/src/Microsoft.Health.Dicom.Api/Features/Responses/MetadataResult.cs b/src/Microsoft.Health.Dicom.Api/Features/Responses/MetadataResult.cs deleted file mode 100644 index 0b94920a9b..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Features/Responses/MetadataResult.cs +++ /dev/null @@ -1,59 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Net; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Health.Dicom.Core.Messages.Retrieve; -using Microsoft.Health.Dicom.Core.Web; -using Microsoft.Net.Http.Headers; - -namespace Microsoft.Health.Dicom.Api.Features.Responses; - -public class MetadataResult : ObjectResult -{ - private readonly RetrieveMetadataResponse _response; - - public MetadataResult(RetrieveMetadataResponse response) - : base(EnsureArg.IsNotNull(response, nameof(response)).ResponseMetadata) - { - _response = response; - } - - public async override Task ExecuteResultAsync(ActionContext context) - { - EnsureArg.IsNotNull(context, nameof(context)); - - ObjectResult result = null; - - // If cache is valid, 304 (Not Modified) status should be returned with no body, else, 200 (OK) status should be returned. - if (_response.IsCacheValid) - { - result = new ObjectResult(null) - { - StatusCode = (int)HttpStatusCode.NotModified, - }; - } - else - { - result = new ObjectResult(_response.ResponseMetadata) - { - StatusCode = (int)HttpStatusCode.OK, - }; - - result.ContentTypes.Add(KnownContentTypes.ApplicationDicomJson); - } - - // If response contains an ETag, add it to the headers. - if (!_response.IsCacheValid && !string.IsNullOrEmpty(_response.ETag)) - { - context.HttpContext.Response.Headers.Append(HeaderNames.ETag, _response.ETag); - } - - await result.ExecuteResultAsync(context); - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Features/Responses/RenderedResult.cs b/src/Microsoft.Health.Dicom.Api/Features/Responses/RenderedResult.cs deleted file mode 100644 index 668be58ea1..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Features/Responses/RenderedResult.cs +++ /dev/null @@ -1,37 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.IO; -using System.Net; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Health.Dicom.Core.Messages.Retrieve; -using Microsoft.Net.Http.Headers; - -namespace Microsoft.Health.Dicom.Api.Features.Responses; -internal class RenderedResult : IActionResult -{ - private readonly RetrieveRenderedResponse _response; - - internal RenderedResult(RetrieveRenderedResponse response) - { - _response = EnsureArg.IsNotNull(response); - } - - public async Task ExecuteResultAsync(ActionContext context) - { - Stream stream = _response.ResponseStream; - context.HttpContext.Response.RegisterForDispose(stream); - var objectResult = new ObjectResult(stream) - { - StatusCode = (int)HttpStatusCode.OK, - }; - var mediaType = new MediaTypeHeaderValue(_response.ContentType); - objectResult.ContentTypes.Add(mediaType); - - await objectResult.ExecuteResultAsync(context); - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Features/Responses/ResourceResult.cs b/src/Microsoft.Health.Dicom.Api/Features/Responses/ResourceResult.cs deleted file mode 100644 index 72670ecfcb..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Features/Responses/ResourceResult.cs +++ /dev/null @@ -1,112 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Health.Dicom.Api.Web; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Messages.Retrieve; -using Microsoft.Health.Dicom.Core.Web; -using Microsoft.Net.Http.Headers; - -namespace Microsoft.Health.Dicom.Api.Features.Responses; - -internal class ResourceResult : IActionResult -{ - private readonly RetrieveResourceResponse _response; - private readonly RetrieveConfiguration _retrieveConfiguration; - - internal ResourceResult(RetrieveResourceResponse response, RetrieveConfiguration retrieveConfiguration) - { - _response = EnsureArg.IsNotNull(response); - _retrieveConfiguration = EnsureArg.IsNotNull(retrieveConfiguration); - } - - public async Task ExecuteResultAsync(ActionContext context) - { - ObjectResult objectResult; - if (_response.IsSinglePart) - { - objectResult = await GetSinglePartResult(context.HttpContext, context.HttpContext.RequestAborted); - } - else - { - objectResult = GetMultiPartResult(context.HttpContext, context.HttpContext.RequestAborted); - } - await objectResult.ExecuteResultAsync(context); - } - - private async Task GetSinglePartResult(HttpContext context, CancellationToken cancellationToken) - { - var enumerator = _response.GetResponseInstancesEnumerator(cancellationToken); - var enumResult = await enumerator.MoveNextAsync(); - - Debug.Assert(enumResult, "Failed to get the item in Enumerator."); - - Stream stream = enumerator.Current.Stream; - string transferSyntax = enumerator.Current.TransferSyntaxUid; - context.Response.RegisterForDispose(stream); - var objectResult = new ObjectResult(stream) - { - StatusCode = (int)HttpStatusCode.OK, - }; - - var singlePartMediaType = new MediaTypeHeaderValue(_response.ContentType); - singlePartMediaType.Parameters.Add(new NameValueHeaderValue(KnownContentTypes.TransferSyntax, transferSyntax)); - - objectResult.ContentTypes.Add(singlePartMediaType); - return objectResult; - } - - private ObjectResult GetMultiPartResult(HttpContext context, CancellationToken cancellationToken) - { - string boundary = Guid.NewGuid().ToString(); - var mediaType = new MediaTypeHeaderValue(KnownContentTypes.MultipartRelated); - mediaType.Parameters.Add(new NameValueHeaderValue(KnownContentTypes.Boundary, boundary)); -#pragma warning disable CA2000 // Dispose objects before losing scope, registered for dispose in response - LazyMultipartReadOnlyStream lazyStream = new LazyMultipartReadOnlyStream( - GetAsyncEnumerableStreamContent(context, _response.GetResponseInstancesEnumerator(cancellationToken), _response.ContentType), - boundary, - _retrieveConfiguration.LazyResponseStreamBufferSize, - cancellationToken); -#pragma warning restore CA2000 // Dispose objects before losing scope - context.Response.RegisterForDispose(lazyStream); - var result = new ObjectResult(lazyStream) - { - StatusCode = (int)HttpStatusCode.OK, - }; - result.ContentTypes.Add(mediaType); - return result; - } - - private static async IAsyncEnumerable GetAsyncEnumerableStreamContent( - HttpContext context, - IAsyncEnumerator lazyInstanceEnumerator, - string contentType) - { - while (await lazyInstanceEnumerator.MoveNextAsync()) - { - context.Response.RegisterForDispose(lazyInstanceEnumerator.Current.Stream); - yield return - new DicomStreamContent() - { - Stream = lazyInstanceEnumerator.Current.Stream, - StreamLength = lazyInstanceEnumerator.Current.StreamLength, - Headers = new List>>() - { - new KeyValuePair>(KnownContentTypes.ContentType, new []{ $"{contentType}; {KnownContentTypes.TransferSyntax}={lazyInstanceEnumerator.Current.TransferSyntaxUid}"}) - } - }; - } - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Features/Routing/KnownActionParameterNames.cs b/src/Microsoft.Health.Dicom.Api/Features/Routing/KnownActionParameterNames.cs deleted file mode 100644 index 4c7371fd11..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Features/Routing/KnownActionParameterNames.cs +++ /dev/null @@ -1,21 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Api.Features.Routing; - -internal static class KnownActionParameterNames -{ - internal const string Version = "version"; - internal const string PartitionName = "partitionName"; - internal const string StudyInstanceUid = "studyInstanceUid"; - internal const string SeriesInstanceUid = "seriesInstanceUid"; - internal const string SopInstanceUid = "sopInstanceUid"; - internal const string WorkItemInstanceUid = "workitemInstanceUid"; - internal const string TransactionUid = "transactionUid"; - internal const string Frames = "frames"; - internal const string Frame = "frame"; - internal const string TagPath = "tagPath"; - internal const string OperationId = "operationId"; -} diff --git a/src/Microsoft.Health.Dicom.Api/Features/Routing/KnownRouteNames.cs b/src/Microsoft.Health.Dicom.Api/Features/Routing/KnownRouteNames.cs deleted file mode 100644 index 7dd732c11a..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Features/Routing/KnownRouteNames.cs +++ /dev/null @@ -1,54 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Api.Features.Routing; - -internal static class KnownRouteNames -{ - internal const string RetrieveStudy = nameof(RetrieveStudy); - internal const string PartitionRetrieveStudy = nameof(PartitionRetrieveStudy); - - internal const string PartitionRetrieveSeries = nameof(PartitionRetrieveSeries); - internal const string RetrieveSeries = nameof(RetrieveSeries); - - internal const string PartitionRetrieveInstance = nameof(PartitionRetrieveInstance); - internal const string RetrieveInstance = nameof(RetrieveInstance); - - internal const string PartitionRetrieveFrame = nameof(PartitionRetrieveFrame); - internal const string RetrieveFrame = nameof(RetrieveFrame); - - internal const string OperationStatus = nameof(OperationStatus); - - internal const string GetExtendedQueryTag = nameof(GetExtendedQueryTag); - - internal const string GetExtendedQueryTagErrors = nameof(GetExtendedQueryTagErrors); - - internal const string PartitionStoreInstance = nameof(PartitionStoreInstance); - internal const string StoreInstance = nameof(StoreInstance); - - internal const string PartitionStoreInstancesInStudy = nameof(PartitionStoreInstancesInStudy); - internal const string StoreInstancesInStudy = nameof(StoreInstancesInStudy); - - internal const string PartitionedAddWorkitemInstance = nameof(PartitionedAddWorkitemInstance); - internal const string AddWorkitemInstance = nameof(AddWorkitemInstance); - - internal const string PartitionSearchWorkitemInstance = nameof(PartitionSearchWorkitemInstance); - internal const string SearchWorkitemInstance = nameof(SearchWorkitemInstance); - - internal const string PartitionedRetrieveWorkitemInstance = nameof(PartitionedRetrieveWorkitemInstance); - internal const string RetrieveWorkitemInstance = nameof(RetrieveWorkitemInstance); - - internal const string PartitionChangeStateWorkitemInstance = nameof(PartitionChangeStateWorkitemInstance); - internal const string ChangeStateWorkitemInstance = nameof(ChangeStateWorkitemInstance); - - internal const string PartitionedUpdateWorkitemInstance = nameof(PartitionedUpdateWorkitemInstance); - internal const string UpdateWorkitemInstance = nameof(UpdateWorkitemInstance); - - internal const string PartitionedCancelWorkitemInstance = nameof(PartitionedCancelWorkitemInstance); - internal const string CancelWorkitemInstance = nameof(CancelWorkitemInstance); - - internal const string PartitionedUpdateInstance = nameof(PartitionedUpdateInstance); - internal const string UpdateInstance = nameof(UpdateInstance); -} diff --git a/src/Microsoft.Health.Dicom.Api/Features/Routing/KnownRoutes.cs b/src/Microsoft.Health.Dicom.Api/Features/Routing/KnownRoutes.cs deleted file mode 100644 index 220ac7fa1c..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Features/Routing/KnownRoutes.cs +++ /dev/null @@ -1,79 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Api.Features.Routing; - -public static class KnownRoutes -{ - private const string PartitionsRouteSegment = "partitions"; - private const string StudiesRouteSegment = "studies"; - private const string SeriesRouteSegment = "series"; - private const string InstancesRouteSegment = "instances"; - private const string MetadataSegment = "metadata"; - private const string RenderedSegment = "rendered"; - private const string ErrorsSegment = "errors"; - private const string ExtendedQueryTagsRouteSegment = "extendedquerytags"; - private const string OperationsSegment = "operations"; - private const string WorkitemsRouteSegment = "workitems"; - private const string WorkitemCancelRequest = "cancelrequest"; - private const string ExportSegment = "export"; - - private const string PartitionNameRouteSegment = "{" + KnownActionParameterNames.PartitionName + "}"; - private const string StudiesInstanceUidRouteSegment = "{" + KnownActionParameterNames.StudyInstanceUid + "}"; - private const string SeriesInstanceUidRouteSegment = "{" + KnownActionParameterNames.SeriesInstanceUid + "}"; - private const string SopInstanceUidRouteSegment = "{" + KnownActionParameterNames.SopInstanceUid + "}"; - private const string FrameIdsRouteSegment = "{" + KnownActionParameterNames.Frames + "}"; - private const string SingleFrameIdRouteSegment = "{" + KnownActionParameterNames.Frame + "}"; - - private const string ExtendedQueryTagPathRouteSegment = "{" + KnownActionParameterNames.TagPath + "}"; - - public const string GetAllPartitionsRoute = PartitionsRouteSegment; - public const string StoreInstancesRoute = StudiesRouteSegment; - public const string StoreInstancesInStudyRoute = StudiesRouteSegment + "/{" + KnownActionParameterNames.StudyInstanceUid + "}"; - - public const string AddWorkitemInstancesRoute = WorkitemsRouteSegment; - public const string RetrieveWorkitemInstancesRoute = WorkitemsRouteSegment + "/{" + KnownActionParameterNames.WorkItemInstanceUid + "}"; - public const string UpdateWorkitemInstancesRoute = WorkitemsRouteSegment + "/{" + KnownActionParameterNames.WorkItemInstanceUid + "}"; - public const string SearchWorkitemInstancesRoute = WorkitemsRouteSegment; - public const string CancelWorkitemInstancesRoute = WorkitemsRouteSegment + "/{" + KnownActionParameterNames.WorkItemInstanceUid + "}/" + WorkitemCancelRequest; - public const string ChangeStateWorkitemInstancesRoute = WorkitemsRouteSegment + "/{" + KnownActionParameterNames.WorkItemInstanceUid + "}/state"; - - public const string PartitionRoute = PartitionsRouteSegment + "/" + PartitionNameRouteSegment; - public const string StudyRoute = StudiesRouteSegment + "/" + StudiesInstanceUidRouteSegment; - public const string SeriesRoute = StudyRoute + "/" + SeriesRouteSegment + "/" + SeriesInstanceUidRouteSegment; - public const string InstanceRoute = SeriesRoute + "/" + InstancesRouteSegment + "/" + SopInstanceUidRouteSegment; - public const string FrameRoute = InstanceRoute + "/frames/" + FrameIdsRouteSegment; - - public const string StudyMetadataRoute = StudyRoute + "/" + MetadataSegment; - public const string SeriesMetadataRoute = SeriesRoute + "/" + MetadataSegment; - public const string InstanceMetadataRoute = InstanceRoute + "/" + MetadataSegment; - - public const string InstanceRenderedRoute = InstanceRoute + "/" + RenderedSegment; - public const string FrameRenderedRoute = InstanceRoute + "/frames/" + SingleFrameIdRouteSegment + "/" + RenderedSegment; - - public const string QueryAllStudiesRoute = StudiesRouteSegment; - public const string QueryAllSeriesRoute = SeriesRouteSegment; - public const string QueryAllInstancesRoute = InstancesRouteSegment; - public const string QuerySeriesInStudyRoute = StudyRoute + "/" + SeriesRouteSegment; - public const string QueryInstancesInStudyRoute = StudyRoute + "/" + InstancesRouteSegment; - public const string QueryInstancesInSeriesRoute = SeriesRoute + "/" + InstancesRouteSegment; - - public const string ExportInstancesRoute = ExportSegment; - - public const string ChangeFeed = "changefeed"; - public const string ChangeFeedLatest = ChangeFeed + "/" + "latest"; - - public const string ExtendedQueryTagRoute = ExtendedQueryTagsRouteSegment; - public const string DeleteExtendedQueryTagRoute = ExtendedQueryTagsRouteSegment + "/" + ExtendedQueryTagPathRouteSegment; - public const string GetExtendedQueryTagRoute = ExtendedQueryTagsRouteSegment + "/" + ExtendedQueryTagPathRouteSegment; - public const string GetExtendedQueryTagErrorsRoute = GetExtendedQueryTagRoute + "/" + ErrorsSegment; - public const string UpdateExtendedQueryTagQueryStatusRoute = GetExtendedQueryTagRoute; - - public const string HealthCheck = "/health/check"; - - public const string OperationInstanceRoute = OperationsSegment + "/{" + KnownActionParameterNames.OperationId + "}"; - - public const string UpdateInstanceRoute = StudiesRouteSegment + "/$bulkUpdate"; -} diff --git a/src/Microsoft.Health.Dicom.Api/Features/Routing/UrlResolver.cs b/src/Microsoft.Health.Dicom.Api/Features/Routing/UrlResolver.cs deleted file mode 100644 index 54b4be9b3f..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Features/Routing/UrlResolver.cs +++ /dev/null @@ -1,178 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using EnsureThat; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.AspNetCore.Mvc.Routing; -using Microsoft.AspNetCore.Routing; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Features.Routing; -using Microsoft.Health.Operations; - -namespace Microsoft.Health.Dicom.Api.Features.Routing; - -public sealed class UrlResolver : IUrlResolver -{ - private readonly IUrlHelperFactory _urlHelperFactory; - - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly IActionContextAccessor _actionContextAccessor; - private readonly LinkGenerator _linkGenerator; - - public UrlResolver( - IUrlHelperFactory urlHelperFactory, - IHttpContextAccessor httpContextAccessor, - IActionContextAccessor actionContextAccessor, - LinkGenerator linkGenerator) - { - EnsureArg.IsNotNull(urlHelperFactory, nameof(urlHelperFactory)); - EnsureArg.IsNotNull(httpContextAccessor, nameof(httpContextAccessor)); - EnsureArg.IsNotNull(actionContextAccessor, nameof(actionContextAccessor)); - - _urlHelperFactory = urlHelperFactory; - _httpContextAccessor = httpContextAccessor; - _actionContextAccessor = actionContextAccessor; - _linkGenerator = linkGenerator; - } - - private ActionContext ActionContext => _actionContextAccessor.ActionContext; - - private IUrlHelper UrlHelper => _urlHelperFactory.GetUrlHelper(ActionContext); - - /// - public Uri ResolveOperationStatusUri(Guid operationId) - { - return RouteUri( - KnownRouteNames.OperationStatus, - new RouteValueDictionary - { - { KnownActionParameterNames.OperationId, operationId.ToString(OperationId.FormatSpecifier) }, - }); - } - - /// - public Uri ResolveQueryTagUri(string tagPath) - { - return RouteUri( - KnownRouteNames.GetExtendedQueryTag, - new RouteValueDictionary - { - { KnownActionParameterNames.TagPath, tagPath }, - }); - } - - /// - public Uri ResolveQueryTagErrorsUri(string tagPath) - { - return RouteUri( - KnownRouteNames.GetExtendedQueryTagErrors, - new RouteValueDictionary - { - { KnownActionParameterNames.TagPath, tagPath }, - }); - } - - /// - public Uri ResolveRetrieveStudyUri(string studyInstanceUid) - { - EnsureArg.IsNotNull(studyInstanceUid, nameof(studyInstanceUid)); - var routeValues = new RouteValueDictionary - { - { KnownActionParameterNames.StudyInstanceUid, studyInstanceUid }, - }; - - AddRouteValues(routeValues, out bool hasPartition); - - var routeName = hasPartition - ? KnownRouteNames.PartitionRetrieveStudy - : KnownRouteNames.RetrieveStudy; - - return RouteUri(routeName, routeValues); - } - - /// - public Uri ResolveRetrieveWorkitemUri(string workitemInstanceUid) - { - EnsureArg.IsNotNull(workitemInstanceUid, nameof(workitemInstanceUid)); - var routeValues = new RouteValueDictionary - { - { KnownActionParameterNames.WorkItemInstanceUid, workitemInstanceUid }, - }; - - AddRouteValues(routeValues, out bool hasPartition); - - var routeName = hasPartition - ? KnownRouteNames.PartitionedRetrieveWorkitemInstance - : KnownRouteNames.RetrieveWorkitemInstance; - - return RouteUri(routeName, routeValues); - } - - /// - public Uri ResolveRetrieveInstanceUri(InstanceIdentifier instanceIdentifier, bool isPartitionEnabled) - { - EnsureArg.IsNotNull(instanceIdentifier, nameof(instanceIdentifier)); - - var routeValues = new RouteValueDictionary - { - { KnownActionParameterNames.StudyInstanceUid, instanceIdentifier.StudyInstanceUid }, - { KnownActionParameterNames.SeriesInstanceUid, instanceIdentifier.SeriesInstanceUid }, - { KnownActionParameterNames.SopInstanceUid, instanceIdentifier.SopInstanceUid }, - }; - - if (isPartitionEnabled) - { - routeValues.Add(KnownActionParameterNames.PartitionName, instanceIdentifier.Partition.Name); - } - - var routeName = isPartitionEnabled - ? KnownRouteNames.PartitionRetrieveInstance - : KnownRouteNames.RetrieveInstance; - - return RouteUri(routeName, routeValues); - } - - private void AddRouteValues(RouteValueDictionary routeValues, out bool hasPartition) - { - hasPartition = _httpContextAccessor.HttpContext.Request.RouteValues.TryGetValue(KnownActionParameterNames.PartitionName, out var partitionName); - - if (hasPartition) - { - routeValues.Add(KnownActionParameterNames.PartitionName, partitionName); - } - } - - private Uri RouteUri(string routeName, RouteValueDictionary routeValues) - { - HttpRequest request = _httpContextAccessor.HttpContext.Request; - - return GetRouteUri( - ActionContext.HttpContext, - routeName, - routeValues, - request.Scheme, - request.Host.Value); - } - - private Uri GetRouteUri(HttpContext httpContext, string routeName, RouteValueDictionary routeValues, string scheme, string host) - { - var uriString = string.Empty; - - if (httpContext == null) - { - uriString = UrlHelper.RouteUrl(routeName, routeValues, scheme, host); - } - else - { - var pathBase = httpContext.Request?.PathBase.ToString(); - uriString = _linkGenerator.GetUriByRouteValues(httpContext, routeName, routeValues, scheme, new HostString(host), pathBase); - } - - return new Uri(uriString); - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Features/Routing/VersionedPartitionRouteAttribute.cs b/src/Microsoft.Health.Dicom.Api/Features/Routing/VersionedPartitionRouteAttribute.cs deleted file mode 100644 index 645aa808a1..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Features/Routing/VersionedPartitionRouteAttribute.cs +++ /dev/null @@ -1,15 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Microsoft.AspNetCore.Mvc; -namespace Microsoft.Health.Dicom.Api.Features.Routing; - -public sealed class VersionedPartitionRouteAttribute : RouteAttribute -{ - public VersionedPartitionRouteAttribute(string template) - : base("/v{version:apiVersion}/" + KnownRoutes.PartitionRoute + "/" + template) - { - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Features/Routing/VersionedRouteAttribute.cs b/src/Microsoft.Health.Dicom.Api/Features/Routing/VersionedRouteAttribute.cs deleted file mode 100644 index 1133df4790..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Features/Routing/VersionedRouteAttribute.cs +++ /dev/null @@ -1,16 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Microsoft.AspNetCore.Mvc; - -namespace Microsoft.Health.Dicom.Api.Features.Routing; - -public sealed class VersionedRouteAttribute : RouteAttribute -{ - public VersionedRouteAttribute(string template) - : base("v{version:apiVersion}/" + template) - { - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Features/Security/QueryStringValidatorMiddleware.cs b/src/Microsoft.Health.Dicom.Api/Features/Security/QueryStringValidatorMiddleware.cs deleted file mode 100644 index b1b3b4f23c..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Features/Security/QueryStringValidatorMiddleware.cs +++ /dev/null @@ -1,41 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.AspNetCore.Http; -using Microsoft.Health.Dicom.Core.Exceptions; - -namespace Microsoft.Health.Dicom.Api.Features.Security; - -internal class QueryStringValidatorMiddleware -{ - private const char UnEncodedLessThan = '<'; - private const string EncodedLessThan = "%3c"; - - private readonly RequestDelegate _next; - - public QueryStringValidatorMiddleware(RequestDelegate next) - { - EnsureArg.IsNotNull(next, nameof(next)); - - _next = next; - } - - public async Task Invoke(HttpContext context) - { - EnsureArg.IsNotNull(context, nameof(context)); - - if (context.Request.QueryString.HasValue - && (context.Request.QueryString.Value.Contains(UnEncodedLessThan, StringComparison.InvariantCulture) - || context.Request.QueryString.Value.Contains(EncodedLessThan, StringComparison.InvariantCultureIgnoreCase))) - { - throw new InvalidQueryStringException(); - } - - await _next(context); - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Features/Security/QueryStringValidatorMiddlewareExtension.cs b/src/Microsoft.Health.Dicom.Api/Features/Security/QueryStringValidatorMiddlewareExtension.cs deleted file mode 100644 index 25fd7b4353..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Features/Security/QueryStringValidatorMiddlewareExtension.cs +++ /dev/null @@ -1,18 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using Microsoft.Health.Dicom.Api.Features.Security; - -namespace Microsoft.AspNetCore.Builder; - -public static class QueryStringValidatorMiddlewareExtension -{ - public static IApplicationBuilder UseQueryStringValidator(this IApplicationBuilder builder) - { - EnsureArg.IsNotNull(builder); - return builder.UseMiddleware(); - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Features/Swagger/ConfigureSwaggerOptions.cs b/src/Microsoft.Health.Dicom.Api/Features/Swagger/ConfigureSwaggerOptions.cs deleted file mode 100644 index 58fd3739de..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Features/Swagger/ConfigureSwaggerOptions.cs +++ /dev/null @@ -1,55 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using Microsoft.AspNetCore.Mvc.ApiExplorer; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Api.Configs; -using Microsoft.OpenApi.Models; -using Swashbuckle.AspNetCore.SwaggerGen; - -namespace Microsoft.Health.Dicom.Api.Features.Swagger; - -public class ConfigureSwaggerOptions : IConfigureOptions -{ - private readonly IApiVersionDescriptionProvider _provider; - private readonly SwaggerConfiguration _swaggerConfiguration; - - public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider, IOptions swaggerConfiguration) - { - _provider = EnsureArg.IsNotNull(provider, nameof(provider)); - _swaggerConfiguration = EnsureArg.IsNotNull(swaggerConfiguration?.Value, nameof(swaggerConfiguration)); - } - - public void Configure(SwaggerGenOptions options) - { - if (_swaggerConfiguration.ServerUri != null) - { - options.AddServer(new OpenApiServer { Url = _swaggerConfiguration.ServerUri.ToString() }); - } - - OpenApiLicense license = null; - if (!string.IsNullOrWhiteSpace(_swaggerConfiguration.License.Name)) - { - license = new OpenApiLicense - { - Name = _swaggerConfiguration.License.Name, - Url = _swaggerConfiguration.License.Url, - }; - } - foreach (ApiVersionDescription description in _provider.ApiVersionDescriptions) - { - options.SwaggerDoc( - description.GroupName, - new OpenApiInfo - { - Title = _swaggerConfiguration.Title, - Version = description.ApiVersion.ToString(), - License = license, - }); - } - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Features/Swagger/ErrorCodeOperationFilter.cs b/src/Microsoft.Health.Dicom.Api/Features/Swagger/ErrorCodeOperationFilter.cs deleted file mode 100644 index 81d1980208..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Features/Swagger/ErrorCodeOperationFilter.cs +++ /dev/null @@ -1,46 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Globalization; -using EnsureThat; -using Microsoft.AspNetCore.Mvc.ApiExplorer; -using Microsoft.OpenApi.Models; -using Swashbuckle.AspNetCore.SwaggerGen; - -namespace Microsoft.Health.Dicom.Api.Features.Swagger; - -public class ErrorCodeOperationFilter : IOperationFilter -{ - public void Apply(OpenApiOperation operation, OperationFilterContext context) - { - EnsureArg.IsNotNull(operation, nameof(operation)); - EnsureArg.IsNotNull(context, nameof(context)); - - foreach (ApiResponseType responseType in context.ApiDescription.SupportedResponseTypes) - { - if (responseType.StatusCode == 400 || responseType.StatusCode == 404 || responseType.StatusCode == 406 || responseType.StatusCode == 415) - { - string responseKey = responseType.IsDefaultResponse ? "default" : responseType.StatusCode.ToString(CultureInfo.InvariantCulture); - - OpenApiResponse response = operation.Responses[responseKey]; - - foreach (string contentType in response.Content.Keys) - { - if (response.Content.Count == 1) - { - OpenApiMediaType value = response.Content[contentType]; - response.Content.Remove(contentType); - response.Content.Add("application/json", value); - break; - } - - response.Content.Remove(contentType); - } - - operation.Responses[responseKey] = response; - } - } - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Features/Swagger/IgnoreEnumSchemaFilter.cs b/src/Microsoft.Health.Dicom.Api/Features/Swagger/IgnoreEnumSchemaFilter.cs deleted file mode 100644 index 8c1c5442f5..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Features/Swagger/IgnoreEnumSchemaFilter.cs +++ /dev/null @@ -1,42 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Text.Json; -using EnsureThat; -using Microsoft.Health.Dicom.Core.Models; -using Microsoft.OpenApi.Any; -using Microsoft.OpenApi.Models; -using Swashbuckle.AspNetCore.SwaggerGen; - -namespace Microsoft.Health.Dicom.Api.Features.Swagger; - -internal class IgnoreEnumSchemaFilter : ISchemaFilter -{ - public void Apply(OpenApiSchema schema, SchemaFilterContext context) - { - EnsureArg.IsNotNull(context, nameof(context)); - EnsureArg.IsNotNull(schema, nameof(schema)); - - if (context.Type.IsEnum) - { - var enumStrings = new List(); - foreach (var value in Enum.GetValues(context.Type)) - { - var member = context.Type.GetMember(value.ToString())[0]; - - if (!member.GetCustomAttributes().Any()) - { - enumStrings.Add(new OpenApiString(JsonNamingPolicy.CamelCase.ConvertName(value.ToString()))); - } - } - - schema.Enum = enumStrings; - } - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Features/Swagger/ReflectionTypeFilter.cs b/src/Microsoft.Health.Dicom.Api/Features/Swagger/ReflectionTypeFilter.cs deleted file mode 100644 index 24d5351cfc..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Features/Swagger/ReflectionTypeFilter.cs +++ /dev/null @@ -1,98 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using Microsoft.OpenApi.Models; -using Swashbuckle.AspNetCore.SwaggerGen; - -namespace Microsoft.Health.Dicom.Api.Features.Swagger; - -// The ReflectionTypeFilter is used to remove types added mistakenly by the Swashbuckle library. -// Swashbuckle mistakenly assumes the DICOM server will serialize Type properties present on the fo-dicom types, -// so this filter removes erroneously added properties in addition to their recursively added data models. -internal class ReflectionTypeFilter : IDocumentFilter -{ - // Get all built-in generic collections (and IEnumerable) - private static readonly HashSet CollectionTypes = typeof(List<>).Assembly - .ExportedTypes - .Where(x => x.Namespace == "System.Collections.Generic") - .Where(x => x == typeof(IEnumerable<>) || x.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>))) - .ToHashSet(); - - public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) - { - HashSet reflectionTypes = GetExposedTypes(typeof(Type)); - - // Resolve the enumerable up-front so that the dictionary can be mutated below - List> schemas = context.SchemaRepository.Schemas.ToList(); - - // Remove all properties, and schemas themselves, whose types are from System.Reflection - foreach (KeyValuePair entry in schemas) - { - if (reflectionTypes.Contains(entry.Key)) - { - context.SchemaRepository.Schemas.Remove(entry.Key); - } - else if (entry.Value.Type == "object" && entry.Value.Properties?.Count > 0) - { - entry.Value.Properties = entry.Value.Properties - .Where(x => x.Value.Reference == null || !reflectionTypes.Contains(x.Value.Reference.Id)) - .ToDictionary(x => x.Key, x => x.Value); - } - } - } - - private static HashSet GetExposedTypes(Type t) - { - var exposedTypes = new HashSet(); - UpdateExposedTypes(t, exposedTypes); - - // The OpenApiSchema type only has the type names - return exposedTypes.Select(x => x.Name).ToHashSet(StringComparer.Ordinal); - } - - private static void UpdateExposedTypes(Type t, HashSet exposed) - { - // Get all public instance properties present in the Type that may be discovered via reflection by Swashbuckle - foreach (Type propertyType in t.GetProperties(BindingFlags.Public | BindingFlags.Instance).Select(x => GetExposedType(x.PropertyType))) - { - if (!IsBuiltInType(propertyType) && exposed.Add(propertyType)) - { - UpdateExposedTypes(propertyType, exposed); - } - } - } - - private static bool IsBuiltInType(Type t) - => (t.IsPrimitive && t != typeof(IntPtr) && t != typeof(UIntPtr)) - || t == typeof(string) - || t == typeof(TimeSpan) - || t == typeof(DateTime) - || t == typeof(DateTimeOffset) - || t == typeof(Guid); - - private static Type GetExposedType(Type t) - { - // If we're serializing a collection to JSON, it will be represented as a JSON array. - // So the type we're really concerned with is the element type contained in the collection. - if (t.IsArray) - { - t = t.GetElementType(); - } - else if (t.IsGenericType && CollectionTypes.Contains(t.GetGenericTypeDefinition())) - { - Type enumerableType = t.GetGenericTypeDefinition() != typeof(IEnumerable<>) - ? t.GetInterfaces().Single(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)) - : t; - - t = enumerableType.GetGenericArguments()[0]; - } - - return t; - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Features/Swagger/RetrieveOperationFilter.cs b/src/Microsoft.Health.Dicom.Api/Features/Swagger/RetrieveOperationFilter.cs deleted file mode 100644 index 9e6e33c7c5..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Features/Swagger/RetrieveOperationFilter.cs +++ /dev/null @@ -1,44 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Globalization; -using EnsureThat; -using Microsoft.AspNetCore.Mvc.ApiExplorer; -using Microsoft.OpenApi.Models; -using Swashbuckle.AspNetCore.SwaggerGen; - -namespace Microsoft.Health.Dicom.Api.Features.Swagger; - -public class RetrieveOperationFilter : IOperationFilter -{ - public void Apply(OpenApiOperation operation, OperationFilterContext context) - { - EnsureArg.IsNotNull(operation, nameof(operation)); - EnsureArg.IsNotNull(context, nameof(context)); - - if (operation.OperationId != null && operation.OperationId.Contains("retrieve", StringComparison.OrdinalIgnoreCase)) - { - foreach (ApiResponseType responseType in context.ApiDescription.SupportedResponseTypes) - { - if (responseType.StatusCode == 200) - { - string responseKey = responseType.IsDefaultResponse ? "default" : responseType.StatusCode.ToString(CultureInfo.InvariantCulture); - - OpenApiResponse response = operation.Responses[responseKey]; - - response.Content.Clear(); - - if (operation.OperationId.EndsWith("Instance", StringComparison.OrdinalIgnoreCase)) - { - response.Content.Add("application/dicom", new OpenApiMediaType()); - } - - response.Content.Add("multipart/related", new OpenApiMediaType()); - } - } - } - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Features/Swagger/SwaggerDefaultValues.cs b/src/Microsoft.Health.Dicom.Api/Features/Swagger/SwaggerDefaultValues.cs deleted file mode 100644 index bff0ed1545..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Features/Swagger/SwaggerDefaultValues.cs +++ /dev/null @@ -1,80 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Globalization; -using System.Linq; -using System.Text.Json; -using EnsureThat; -using Microsoft.AspNetCore.Mvc.ApiExplorer; -using Microsoft.OpenApi.Models; -using Swashbuckle.AspNetCore.SwaggerGen; - -namespace Microsoft.Health.Dicom.Api.Features.Swagger; - -/// -/// Represents the Swagger/Swashbuckle operation filter used to document the implicit API version parameter. -/// -/// This is only required due to bugs in the . -/// Once they are fixed and published, this class can be removed. Fix was found from the samples provided: -/// https://github.com/microsoft/aspnet-api-versioning/blob/master/samples/aspnetcore/SwaggerSample/SwaggerDefaultValues.cs -public class SwaggerDefaultValues : IOperationFilter -{ - /// - /// Applies the filter to the specified operation using the given context. - /// - /// The operation to apply the filter to. - /// The current operation filter context. - public void Apply(OpenApiOperation operation, OperationFilterContext context) - { - EnsureArg.IsNotNull(operation, nameof(operation)); - EnsureArg.IsNotNull(context, nameof(context)); - - ApiDescription apiDescription = context.ApiDescription; - - operation.Deprecated |= apiDescription.IsDeprecated(); - - // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/1752#issue-663991077 - foreach (ApiResponseType responseType in context.ApiDescription.SupportedResponseTypes) - { - // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/b7cf75e7905050305b115dd96640ddd6e74c7ac9/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs#L383-L387 - string responseKey = responseType.IsDefaultResponse ? "default" : responseType.StatusCode.ToString(CultureInfo.InvariantCulture); - OpenApiResponse response = operation.Responses[responseKey]; - - foreach (string contentType in response.Content.Keys) - { - if (!responseType.ApiResponseFormats.Any(x => x.MediaType == contentType)) - { - response.Content.Remove(contentType); - } - } - - operation.Responses[responseKey] = response; - } - - if (operation.Parameters == null) - { - return; - } - - foreach (OpenApiParameter parameter in operation.Parameters) - { - ApiParameterDescription description = apiDescription.ParameterDescriptions.First(p => p.Name == parameter.Name); - - if (parameter.Description == null) - { - parameter.Description = description.ModelMetadata?.Description; - } - - if (parameter.Schema.Default == null && description.DefaultValue != null) - { - // REF: https://github.com/Microsoft/aspnet-api-versioning/issues/429#issuecomment-605402330 - string json = JsonSerializer.Serialize(description.DefaultValue, description.ModelMetadata.ModelType); - parameter.Schema.Default = OpenApiAnyFactory.CreateFromJson(json); - } - - parameter.Required |= description.IsRequired; - } - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Features/Telemetry/DicomTelemetry.cs b/src/Microsoft.Health.Dicom.Api/Features/Telemetry/DicomTelemetry.cs deleted file mode 100644 index 908ffd7a23..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Features/Telemetry/DicomTelemetry.cs +++ /dev/null @@ -1,11 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Api.Features.Telemetry; - -internal static class DicomTelemetry -{ - public const string ContextItemPrefix = "Dicom_"; -} diff --git a/src/Microsoft.Health.Dicom.Api/Features/Telemetry/HttpDicomTelemetryClient.cs b/src/Microsoft.Health.Dicom.Api/Features/Telemetry/HttpDicomTelemetryClient.cs deleted file mode 100644 index ef138a5b10..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Features/Telemetry/HttpDicomTelemetryClient.cs +++ /dev/null @@ -1,37 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using Microsoft.ApplicationInsights; -using Microsoft.AspNetCore.Http; -using Microsoft.Health.Dicom.Core.Features.Telemetry; - -namespace Microsoft.Health.Dicom.Api.Features.Telemetry; - -internal class HttpDicomTelemetryClient : IDicomTelemetryClient -{ - private readonly TelemetryClient _telemetryClient; - private readonly IHttpContextAccessor _httpContextAccessor; - - public HttpDicomTelemetryClient(TelemetryClient telemetryClient, IHttpContextAccessor httpContextAccessor) - { - _telemetryClient = EnsureArg.IsNotNull(telemetryClient, nameof(telemetryClient)); - _httpContextAccessor = EnsureArg.IsNotNull(httpContextAccessor, nameof(httpContextAccessor)); - } - - public void TrackMetric(string name, int value) - { - // Note: Context Items are prefixed so that the telemetry initializer knows which items to include in the telemetry - _httpContextAccessor.HttpContext.Items[DicomTelemetry.ContextItemPrefix + name] = value; - _telemetryClient.GetMetric(name).TrackValue(value); - } - - public void TrackMetric(string name, long value) - { - // Note: Context Items are prefixed so that the telemetry initializer knows which items to include in the telemetry - _httpContextAccessor.HttpContext.Items[DicomTelemetry.ContextItemPrefix + name] = value; - _telemetryClient.GetMetric(name).TrackValue(value); - } -} diff --git a/src/Microsoft.Health.Dicom.Api/GlobalSuppressions.cs b/src/Microsoft.Health.Dicom.Api/GlobalSuppressions.cs deleted file mode 100644 index d4816082ba..0000000000 --- a/src/Microsoft.Health.Dicom.Api/GlobalSuppressions.cs +++ /dev/null @@ -1,13 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -// This file is used by Code Analysis to maintain SuppressMessage -// attributes that are applied to this project. -// Project-level suppressions either have no target or are given -// a specific target and scoped to a namespace, type, member, etc. - -using System.Diagnostics.CodeAnalysis; - -[assembly: SuppressMessage("Design", "CA1019:Define accessors for attribute arguments", Justification = "ASP.NET Core attributes leverage arguments that they do not necessarily wish to expose.")] diff --git a/src/Microsoft.Health.Dicom.Api/Logging/TelemetryInitializer.cs b/src/Microsoft.Health.Dicom.Api/Logging/TelemetryInitializer.cs deleted file mode 100644 index cb84a0803a..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Logging/TelemetryInitializer.cs +++ /dev/null @@ -1,101 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using EnsureThat; -using Microsoft.ApplicationInsights.Channel; -using Microsoft.ApplicationInsights.DataContracts; -using Microsoft.ApplicationInsights.Extensibility; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc.Versioning; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Api.Features.Telemetry; -using Microsoft.Health.Dicom.Core.Configs; - -namespace Microsoft.Health.Dicom.Api.Logging; - -/// -/// Class extends AppInsights telemtry to include custom properties -/// -internal class TelemetryInitializer : ITelemetryInitializer -{ - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly bool _enableDataPartitions; - private readonly bool _enableExport; - private readonly bool _enableExternalStore; - private const string ApiVersionColumnName = "ApiVersion"; - private const string EnableDataPartitions = "EnableDataPartitions"; - private const string EnableExport = "EnableExport"; - private const string EnableExternalStore = "EnableExternalStore"; - private const string UserAgent = "UserAgent"; - - public TelemetryInitializer(IHttpContextAccessor httpContextAccessor, IOptions featureConfiguration) - { - _httpContextAccessor = EnsureArg.IsNotNull(httpContextAccessor, nameof(httpContextAccessor)); - EnsureArg.IsNotNull(featureConfiguration?.Value, nameof(featureConfiguration)); - _enableDataPartitions = featureConfiguration.Value.EnableDataPartitions; - _enableExport = featureConfiguration.Value.EnableExport; - _enableExternalStore = featureConfiguration.Value.EnableExternalStore; - } - - public void Initialize(ITelemetry telemetry) - { - AddMetadataColumns(telemetry); - if (telemetry is RequestTelemetry requestTelemetry) - AddPropertiesFromHttpContextItems(requestTelemetry); - } - - private void AddMetadataColumns(ITelemetry telemetry) - { - var requestTelemetry = telemetry as RequestTelemetry; - if (requestTelemetry == null) - { - return; - } - - string version = null; - var feature = _httpContextAccessor.HttpContext?.Features.Get(); - - if (feature?.RouteParameter != null) - { - version = feature.RawRequestedApiVersion; - } - - if (version == null) - { - return; - } - - requestTelemetry.Properties[ApiVersionColumnName] = version; - requestTelemetry.Properties[EnableDataPartitions] = _enableDataPartitions.ToString(); - requestTelemetry.Properties[EnableExport] = _enableExport.ToString(); - requestTelemetry.Properties[EnableExternalStore] = _enableExternalStore.ToString(); - requestTelemetry.Properties[UserAgent] = _httpContextAccessor.HttpContext?.Request.Headers.UserAgent; - } - - private void AddPropertiesFromHttpContextItems(RequestTelemetry requestTelemetry) - { - if (_httpContextAccessor.HttpContext == null) - { - return; - } - - IEnumerable<(string Key, string Value)> properties = _httpContextAccessor.HttpContext - .Items - .Select(x => (Key: x.Key.ToString(), x.Value)) - .Where(x => x.Key.StartsWith(DicomTelemetry.ContextItemPrefix, StringComparison.Ordinal)) - .Select(x => (x.Key[DicomTelemetry.ContextItemPrefix.Length..], x.Value?.ToString())); - - foreach ((string key, string value) in properties) - { - if (requestTelemetry.Properties.ContainsKey(key)) - requestTelemetry.Properties["DuplicateDimension"] = bool.TrueString; - - requestTelemetry.Properties[key] = value; - } - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Microsoft.Health.Dicom.Api.csproj b/src/Microsoft.Health.Dicom.Api/Microsoft.Health.Dicom.Api.csproj deleted file mode 100644 index bc64db22c5..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Microsoft.Health.Dicom.Api.csproj +++ /dev/null @@ -1,61 +0,0 @@ - - - - Common components, such as controllers, for Microsoft's DICOMweb APIs using ASP.NET Core. - $(LatestVersion) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - WorkitemController.cs - - - - - - True - True - DicomApiResource.resx - - - ResXFileCodeGenerator - DicomApiResource.Designer.cs - - - - diff --git a/src/Microsoft.Health.Dicom.Api/Models/PaginationOptions.cs b/src/Microsoft.Health.Dicom.Api/Models/PaginationOptions.cs deleted file mode 100644 index 2418f0a9f8..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Models/PaginationOptions.cs +++ /dev/null @@ -1,18 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.ComponentModel.DataAnnotations; - -namespace Microsoft.Health.Dicom.Api.Models; - -public class PaginationOptions -{ - [Range(0, long.MaxValue)] - public long Offset { get; set; } - - [Range(1, 200)] - public int Limit { get; set; } = 100; -} diff --git a/src/Microsoft.Health.Dicom.Api/Models/PartitionEntry.cs b/src/Microsoft.Health.Dicom.Api/Models/PartitionEntry.cs deleted file mode 100644 index cecd15abc3..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Models/PartitionEntry.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using Microsoft.Health.Dicom.Core.Features.Partitioning; - -namespace Microsoft.Health.Dicom.Api.Models; - -/// -/// Used to ensure API contract does not change while we change the underlying property naming. -/// -public class PartitionEntry : Partition -{ - public PartitionEntry(int key, string name, DateTimeOffset createdDate = default) : base(key, name, createdDate) - { - } -} \ No newline at end of file diff --git a/src/Microsoft.Health.Dicom.Api/Models/QueryOptions.cs b/src/Microsoft.Health.Dicom.Api/Models/QueryOptions.cs deleted file mode 100644 index 9e572ac367..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Models/QueryOptions.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Health.Dicom.Api.Features.ModelBinders; - -namespace Microsoft.Health.Dicom.Api.Models; - -public class QueryOptions : PaginationOptions -{ - public bool FuzzyMatching { get; set; } - - [ModelBinder(typeof(AggregateCsvModelBinder))] - public IReadOnlyList IncludeField { get; set; } = Array.Empty(); -} diff --git a/src/Microsoft.Health.Dicom.Api/Models/UpdateExtendedQueryTagOptions.cs b/src/Microsoft.Health.Dicom.Api/Models/UpdateExtendedQueryTagOptions.cs deleted file mode 100644 index d9aa7831be..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Models/UpdateExtendedQueryTagOptions.cs +++ /dev/null @@ -1,44 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Linq; -using System.Text.Json; -using System.Text.Json.Serialization; -using Microsoft.Health.Dicom.Api.Features.Converter; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -namespace Microsoft.Health.Dicom.Api.Models; - -public class UpdateExtendedQueryTagOptions : IValidatableObject -{ - /// - /// Gets or sets query status. - /// - [Required] - [JsonConverter(typeof(EnumNameJsonConverter))] - public QueryStatus? QueryStatus { get; set; } - - [JsonExtensionData] - [SuppressMessage("Design", "CA2227:Collection properties should be read only", Justification = "Used by JsonDeserializer to store extension data.")] - public IDictionary ExtensionData { get; set; } - - public UpdateExtendedQueryTagEntry ToEntry() - { - return new UpdateExtendedQueryTagEntry(QueryStatus.Value); - } - - public IEnumerable Validate(ValidationContext validationContext) - { - return ExtensionData != null && ExtensionData.Count != 0 - ? ExtensionData.Select(x => new ValidationResult(string.Format(CultureInfo.InvariantCulture, DicomApiResource.UnsupportedField, x.Key), new[] { x.Key })) - : Array.Empty(); - - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Models/WindowedPaginationOptions.cs b/src/Microsoft.Health.Dicom.Api/Models/WindowedPaginationOptions.cs deleted file mode 100644 index 13dbdb88f2..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Models/WindowedPaginationOptions.cs +++ /dev/null @@ -1,41 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Globalization; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Health.Dicom.Api.Features.ModelBinders; -using Microsoft.Health.Dicom.Core.Models; - -namespace Microsoft.Health.Dicom.Api.Models; - -public class WindowedPaginationOptions : PaginationOptions, IValidatableObject -{ - [ModelBinder(typeof(MandatoryTimeZoneBinder))] - public DateTimeOffset? StartTime { get; set; } - - [ModelBinder(typeof(MandatoryTimeZoneBinder))] - public DateTimeOffset? EndTime { get; set; } - - public TimeRange Window => new TimeRange(StartTime.GetValueOrDefault(DateTimeOffset.MinValue), EndTime.GetValueOrDefault(DateTimeOffset.MaxValue)); - - public IEnumerable Validate(ValidationContext validationContext) - { - DateTimeOffset start = StartTime.GetValueOrDefault(DateTimeOffset.MinValue); - DateTimeOffset end = EndTime.GetValueOrDefault(DateTimeOffset.MaxValue); - - if (end <= start) - { - yield return new ValidationResult( - string.Format( - CultureInfo.InvariantCulture, - DicomApiResource.InvalidTimeRange, - start, - end)); - } - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Modules/AuditModule.cs b/src/Microsoft.Health.Dicom.Api/Modules/AuditModule.cs deleted file mode 100644 index f8f52521e1..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Modules/AuditModule.cs +++ /dev/null @@ -1,39 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Health.Api.Features.Audit; -using Microsoft.Health.Dicom.Core.Features.Audit; -using Microsoft.Health.Extensions.DependencyInjection; -using DicomAudit = Microsoft.Health.Dicom.Api.Features.Audit; - -namespace Microsoft.Health.Dicom.Api.Modules; - -public class AuditModule : IStartupModule -{ - public void Load(IServiceCollection services) - { - EnsureArg.IsNotNull(services, nameof(services)); - - services.Add() - .Singleton() - .AsSelf(); - - services.AddSingleton(); - - services.AddSingleton(); - - services.Add() - .Singleton() - .AsService(); - - services.Add() - .Singleton() - .AsService() - .AsService(); - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Modules/MvcModule.cs b/src/Microsoft.Health.Dicom.Api/Modules/MvcModule.cs deleted file mode 100644 index e5b4309861..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Modules/MvcModule.cs +++ /dev/null @@ -1,39 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Health.Dicom.Api.Features.Filters; -using Microsoft.Health.Extensions.DependencyInjection; - -namespace Microsoft.Health.Dicom.Api.Modules; - -public class MvcModule : IStartupModule -{ - public void Load(IServiceCollection services) - { - EnsureArg.IsNotNull(services, nameof(services)); - - services.PostConfigure(options => - { - // This filter should run first because it populates data for DicomRequestContext. - options.Filters.Add(typeof(DicomRequestContextRouteDataPopulatingFilterAttribute), 0); - }); - - services.AddHttpContextAccessor(); - - // These are needed for IUrlResolver. If it's no longer need it, - // we should remove the registration since enabling these accessors has performance implications. - // https://github.com/aspnet/Hosting/issues/793 - services.TryAddSingleton(); - - services.Add() - .Scoped() - .AsSelf(); - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Modules/SecurityModule.cs b/src/Microsoft.Health.Dicom.Api/Modules/SecurityModule.cs deleted file mode 100644 index 015abf6e08..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Modules/SecurityModule.cs +++ /dev/null @@ -1,122 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.IdentityModel.Tokens.Jwt; -using System.Linq; -using EnsureThat; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc.Authorization; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Health.Core.Features.Context; -using Microsoft.Health.Core.Features.Security; -using Microsoft.Health.Core.Features.Security.Authorization; -using Microsoft.Health.Dicom.Api.Configs; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Features.Context; -using Microsoft.Health.Dicom.Core.Features.Security; -using Microsoft.Health.Extensions.DependencyInjection; -using Microsoft.IdentityModel.Tokens; - -namespace Microsoft.Health.Dicom.Api.Modules; - -public class SecurityModule : IStartupModule -{ - private readonly SecurityConfiguration _securityConfiguration; - - public SecurityModule(DicomServerConfiguration dicomServerConfiguration) - { - EnsureArg.IsNotNull(dicomServerConfiguration, nameof(dicomServerConfiguration)); - _securityConfiguration = dicomServerConfiguration.Security; - } - - public void Load(IServiceCollection services) - { - EnsureArg.IsNotNull(services, nameof(services)); - - // Set the token handler to not do auto inbound mapping. (e.g. "roles" -> "http://schemas.microsoft.com/ws/2008/06/identity/claims/role") - // The JWT security token handler has a new property MapInboundClaims which is set to true by default. - // When this property is true, it maps some claim types to Microsoft's proprietary ones. - // This includes mapping the standard JWT "roles" claim to ClaimTypes.Role. - // If you want to keep the "roles" claim as is, you need to set MapInboundClaims to false - // In .Net 7, JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); works - // Im .Net 8. JwtSecurityTokenHandler.DefaultMapInboundClaims = false; works - JwtSecurityTokenHandler.DefaultMapInboundClaims = false; - - if (_securityConfiguration.Enabled) - { - string[] validAudiences = GetValidAudiences(); - string challengeAudience = validAudiences?.FirstOrDefault(); - - services.AddAuthentication(options => - { - options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; - options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; - options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; - }) - .AddJwtBearer(options => - { - options.Authority = _securityConfiguration.Authentication.Authority; - options.RequireHttpsMetadata = true; - options.Challenge = $"Bearer authorization_uri=\"{_securityConfiguration.Authentication.Authority}\", resource_id=\"{challengeAudience}\", realm=\"{challengeAudience}\""; - options.TokenValidationParameters = new TokenValidationParameters - { - ValidAudiences = validAudiences, - }; - }); - - services.AddControllers(mvcOptions => - { - var policy = new AuthorizationPolicyBuilder() - .RequireAuthenticatedUser() - .Build(); - - mvcOptions.Filters.Add(new AuthorizeFilter(policy)); - }); - - if (_securityConfiguration.Authorization.Enabled) - { - services.Add().Transient().AsImplementedInterfaces(); - services.AddSingleton(_securityConfiguration.Authorization); - - services.AddSingleton, RoleBasedAuthorizationService>(); - } - else - { - services.AddSingleton, DisabledAuthorizationService>(); - } - } - else - { - services.AddSingleton, DisabledAuthorizationService>(); - } - - services.Add() - .Singleton() - .AsSelf() - .AsService>() - .AsService(); - - services.AddSingleton(); - } - - internal string[] GetValidAudiences() - { - if (_securityConfiguration.Authentication.Audiences != null) - { - return _securityConfiguration.Authentication.Audiences.ToArray(); - } - - if (!string.IsNullOrWhiteSpace(_securityConfiguration.Authentication.Audience)) - { - return new[] - { - _securityConfiguration.Authentication.Audience, - }; - } - - return null; - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Modules/WebModule.cs b/src/Microsoft.Health.Dicom.Api/Modules/WebModule.cs deleted file mode 100644 index 58761d0650..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Modules/WebModule.cs +++ /dev/null @@ -1,29 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Health.Dicom.Api.Web; -using Microsoft.Health.Extensions.DependencyInjection; - -namespace Microsoft.Health.Dicom.Api.Modules; - -public class WebModule : IStartupModule -{ - public void Load(IServiceCollection services) - { - EnsureArg.IsNotNull(services, nameof(services)); - - services.Add() - .Singleton() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Singleton() - .AsSelf() - .AsImplementedInterfaces(); - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Properties/AssemblyInfo.cs b/src/Microsoft.Health.Dicom.Api/Properties/AssemblyInfo.cs deleted file mode 100644 index a1f73c8539..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,13 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Resources; -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("Microsoft.Health.Dicom.Api.UnitTests")] -[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] -[assembly: NeutralResourcesLanguage("en-us")] -[assembly: CLSCompliant(false)] diff --git a/src/Microsoft.Health.Dicom.Api/Registration/DicomServerServiceCollectionExtensions.cs b/src/Microsoft.Health.Dicom.Api/Registration/DicomServerServiceCollectionExtensions.cs deleted file mode 100644 index 26c05fbfbf..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Registration/DicomServerServiceCollectionExtensions.cs +++ /dev/null @@ -1,261 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using EnsureThat; -using Microsoft.ApplicationInsights.Extensibility; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Mvc.ApiExplorer; -using Microsoft.AspNetCore.Mvc.Versioning; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Options; -using Microsoft.Health.Api.Features.Audit; -using Microsoft.Health.Api.Features.Context; -using Microsoft.Health.Api.Features.Cors; -using Microsoft.Health.Api.Features.Headers; -using Microsoft.Health.Api.Modules; -using Microsoft.Health.Core.Features.Health; -using Microsoft.Health.Dicom.Api.Configs; -using Microsoft.Health.Dicom.Api.Features.BackgroundServices; -using Microsoft.Health.Dicom.Api.Features.Context; -using Microsoft.Health.Dicom.Api.Features.Conventions; -using Microsoft.Health.Dicom.Api.Features.Partitioning; -using Microsoft.Health.Dicom.Api.Features.Routing; -using Microsoft.Health.Dicom.Api.Features.Swagger; -using Microsoft.Health.Dicom.Api.Features.Telemetry; -using Microsoft.Health.Dicom.Api.Logging; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Context; -using Microsoft.Health.Dicom.Core.Features.FellowOakDicom; -using Microsoft.Health.Dicom.Core.Features.Routing; -using Microsoft.Health.Dicom.Core.Features.Telemetry; -using Microsoft.Health.Dicom.Core.Registration; -using Microsoft.Health.Encryption.Customer.Configs; -using Microsoft.Health.Encryption.Customer.Extensions; -using Microsoft.Health.Extensions.DependencyInjection; -using Swashbuckle.AspNetCore.SwaggerGen; - -namespace Microsoft.AspNetCore.Builder; - -public static class DicomServerServiceCollectionExtensions -{ - private const string DicomServerConfigurationSectionName = "DicomServer"; - - /// - /// Add services for DICOM background workers. - /// - /// The DICOM server builder instance. - /// The configuration for the DICOM server. - /// The DICOM server builder instance. - public static IDicomServerBuilder AddBackgroundWorkers(this IDicomServerBuilder serverBuilder, IConfiguration configuration) - { - EnsureArg.IsNotNull(serverBuilder, nameof(serverBuilder)); - EnsureArg.IsNotNull(configuration, nameof(configuration)); - - FeatureConfiguration featureConfiguration = new FeatureConfiguration(); - configuration.GetSection("DicomServer").GetSection("Features").Bind(featureConfiguration); - - serverBuilder.Services.AddScoped(); - serverBuilder.Services.AddHostedService(); - if (featureConfiguration.EnableExternalStore) - { - serverBuilder.Services.AddHostedService(); - } - - HealthCheckPublisherConfiguration healthCheckPublisherConfiguration = new HealthCheckPublisherConfiguration(); - configuration.GetSection(HealthCheckPublisherConfiguration.SectionName).Bind(healthCheckPublisherConfiguration); - IReadOnlyList excludedHealthCheckNames = healthCheckPublisherConfiguration.GetListOfExcludedHealthCheckNames(); - - serverBuilder.Services - .AddCustomerKeyValidationBackgroundService(options => configuration - .GetSection(CustomerManagedKeyOptions.CustomerManagedKey) - .Bind(options)) - .AddHealthCheckCachePublisher(options => - { - configuration - .GetSection(HealthCheckPublisherConfiguration.SectionName) - .Bind(options); - - options.Predicate = (check) => !excludedHealthCheckNames.Contains(check.Name); - }); - - return serverBuilder; - } - - /// - /// Add services for DICOM hosted services. - /// - /// The DICOM server builder instance. - /// The DICOM server builder instance. - public static IDicomServerBuilder AddHostedServices(this IDicomServerBuilder serverBuilder) - { - EnsureArg.IsNotNull(serverBuilder, nameof(serverBuilder)); - serverBuilder.Services.AddHostedService(); - return serverBuilder; - } - - /// - /// Adds services for enabling a DICOM server. - /// - /// The services collection. - /// An optional configuration root object. This method uses the "DicomServer" section. - /// An optional delegate to set properties after values have been loaded from configuration. - /// A object. - public static IDicomServerBuilder AddDicomServer( - this IServiceCollection services, - IConfiguration configurationRoot, - Action configureAction = null) - { - EnsureArg.IsNotNull(services, nameof(services)); - - var dicomServerConfiguration = new DicomServerConfiguration(); - - configurationRoot?.GetSection(DicomServerConfigurationSectionName).Bind(dicomServerConfiguration); - configureAction?.Invoke(dicomServerConfiguration); - - var featuresOptions = Options.Create(dicomServerConfiguration.Features); - services.AddSingleton(Options.Create(dicomServerConfiguration)); - services.AddSingleton(Options.Create(dicomServerConfiguration.Security)); - services.AddSingleton(featuresOptions); - services.AddSingleton(Options.Create(dicomServerConfiguration.Services.DeletedInstanceCleanup)); - services.AddSingleton(Options.Create(dicomServerConfiguration.Services.StoreServiceSettings)); - services.AddSingleton(Options.Create(dicomServerConfiguration.Services.ExtendedQueryTag)); - services.AddSingleton(Options.Create(dicomServerConfiguration.Services.DataPartition)); - services.AddSingleton(Options.Create(dicomServerConfiguration.Audit)); - services.AddSingleton(Options.Create(dicomServerConfiguration.Swagger)); - services.AddSingleton(Options.Create(dicomServerConfiguration.Services.Retrieve)); - services.AddSingleton(Options.Create(dicomServerConfiguration.Services.InstanceMetadataCacheConfiguration)); - services.AddSingleton(Options.Create(dicomServerConfiguration.Services.FramesRangeCacheConfiguration)); - services.AddSingleton(Options.Create(dicomServerConfiguration.Services.UpdateServiceSettings)); - services.AddSingleton(Options.Create(dicomServerConfiguration.Services.DataCleanupConfiguration)); - services.AddSingleton(Options.Create(dicomServerConfiguration.Services.ContentLengthBackFillConfiguration)); - - services.RegisterAssemblyModules(Assembly.GetExecutingAssembly(), dicomServerConfiguration); - services.RegisterAssemblyModules(typeof(InitializationModule).Assembly, dicomServerConfiguration); - services.AddApplicationInsightsTelemetry(); - - services.AddOptions(); - - services - .AddControllers(options => - { - options.EnableEndpointRouting = true; - options.RespectBrowserAcceptHeader = true; - }) - .AddJsonSerializerOptions(o => o.ConfigureDefaultDicomSettings()); - - services.AddApiVersioning(c => - { - c.ApiVersionReader = new UrlSegmentApiVersionReader(); - c.AssumeDefaultVersionWhenUnspecified = true; - c.ReportApiVersions = true; - c.UseApiBehavior = false; - - c.Conventions.Add(new ApiVersionsConvention(featuresOptions)); - }); - - services.AddVersionedApiExplorer(options => - { - // The format for this is 'v'major[.minor][-status] ex. v1.0-prerelease - options.GroupNameFormat = "'v'VVV"; - options.SubstituteApiVersionInUrl = true; - }); - - services.AddTransient, ConfigureSwaggerOptions>(); - services.AddSwaggerGen(options => - { - options.OperationFilter(); - options.OperationFilter(); - options.OperationFilter(); - options.DocumentFilter(); - options.SchemaFilter(); - }); - - services.AddSingleton(); - - services.RegisterAssemblyModules(typeof(DicomMediatorExtensions).Assembly, dicomServerConfiguration.Features, dicomServerConfiguration.Services); - services.AddTransient(); - - services.AddRecyclableMemoryStreamManager(configurationRoot); - - services.AddSingleton(); - services.AddSingleton(); - - CustomDicomImplementation.SetDicomImplementationClassUIDAndVersion(); - - return new DicomServerBuilder(services); - } - - private class DicomServerBuilder : IDicomServerBuilder - { - public DicomServerBuilder(IServiceCollection services) - { - EnsureArg.IsNotNull(services, nameof(services)); - Services = services; - } - - public IServiceCollection Services { get; } - } - - /// - /// An that configures middleware components before any components are added in Startup.Configure - /// - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1812:Avoid uninstantiated internal classes.", Justification = "This class is instantiated.")] - private class DicomServerStartupFilter : IStartupFilter - { - public Action Configure(Action next) - { - return app => - { - IWebHostEnvironment env = app.ApplicationServices.GetRequiredService(); - - IApiVersionDescriptionProvider provider = app.ApplicationServices.GetRequiredService(); - - // This middleware will add delegates to the OnStarting method of httpContext.Response for setting headers. - app.UseBaseHeaders(); - - app.UseCors(CorsConstants.DefaultCorsPolicy); - - app.UseDicomRequestContext(); - - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseAudit(); - - app.UseExceptionHandling(); - - app.UseAuthentication(); - - app.UseRequestContextAfterAuthentication(); - - // Dependency on URSA scan. We should see how other teams do this. - app.UseSwagger(c => - { - c.RouteTemplate = "{documentName}/api.{json|yaml}"; - }); - - //Disabling swagger ui until accesability team gets back to us - //app.UseSwaggerUI(options => - //{ - // foreach (ApiVersionDescription description in provider.ApiVersionDescriptions) - // { - // options.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.yaml", description.GroupName.ToUpperInvariant()); - // } - //}); - - next(app); - }; - } - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Registration/JsonMvcBuilderExtensions.cs b/src/Microsoft.Health.Dicom.Api/Registration/JsonMvcBuilderExtensions.cs deleted file mode 100644 index a6e57761a2..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Registration/JsonMvcBuilderExtensions.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Text.Json; -using EnsureThat; - -namespace Microsoft.Extensions.DependencyInjection; - -internal static class JsonMvcBuilderExtensions -{ - public static IMvcBuilder AddJsonSerializerOptions(this IMvcBuilder builder, Action configure) - { - EnsureArg.IsNotNull(builder, nameof(builder)); - EnsureArg.IsNotNull(configure, nameof(configure)); - - builder.AddJsonOptions(o => configure(o.JsonSerializerOptions)); - builder.Services.Configure(configure); - - return builder; - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Registration/OhifViewerExtensions.cs b/src/Microsoft.Health.Dicom.Api/Registration/OhifViewerExtensions.cs deleted file mode 100644 index 080795dd95..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Registration/OhifViewerExtensions.cs +++ /dev/null @@ -1,35 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Rewrite; - -namespace Microsoft.Health.Dicom.Api.Registration; - -public static class OhifViewerExtensions -{ - private const string OhifViewerIndexPagePath = "index.html"; - - /// - /// Enable OHIF viewer. - /// - public static void UseOhifViewer(this IApplicationBuilder app) - { - // In order to make OHIF viewer work with direct link to studies, we need to rewrite any path under viewer - // back to the index page so the viewer can display accordingly. - RewriteOptions rewriteOptions = new RewriteOptions() - .AddRewrite("^viewer/(.*?)", OhifViewerIndexPagePath, true); - - app.UseRewriter(rewriteOptions); - - var options = new DefaultFilesOptions(); - - options.DefaultFileNames.Clear(); - options.DefaultFileNames.Add(OhifViewerIndexPagePath); - - app.UseDefaultFiles(options); - app.UseStaticFiles(); - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Web/AspNetCoreMultipartReader.cs b/src/Microsoft.Health.Dicom.Api/Web/AspNetCoreMultipartReader.cs deleted file mode 100644 index f4f4f78126..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Web/AspNetCoreMultipartReader.cs +++ /dev/null @@ -1,158 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Globalization; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.WebUtilities; -using Microsoft.Extensions.Options; -using Microsoft.Health.Abstractions.Exceptions; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Web; -using Microsoft.Net.Http.Headers; -using NotSupportedException = Microsoft.Health.Dicom.Core.Exceptions.NotSupportedException; - -namespace Microsoft.Health.Dicom.Api.Web; - -/// -/// Multipart reader implemented by using AspNetCore's . -/// -internal class AspNetCoreMultipartReader : IMultipartReader -{ - /// - /// This is the exception message that will be returned by both IIS - /// and Kestrel - /// in the case of the MultipartReader attempting to read - /// past the request body limit configured at the server level. - /// - private const string BodyTooLargeExceptionMessage = "Request body too large."; - - private const string TypeParameterName = "type"; - private const string StartParameterName = "start"; - private readonly ISeekableStreamConverter _seekableStreamConverter; - private readonly IOptions _storeConfiguration; - private readonly string _rootContentType; - private readonly MultipartReader _multipartReader; - - private int _sectionIndex; - - internal AspNetCoreMultipartReader( - string contentType, - Stream body, - ISeekableStreamConverter seekableStreamConverter, - IOptions storeConfiguration) - { - EnsureArg.IsNotNull(contentType, nameof(contentType)); - EnsureArg.IsNotNull(body, nameof(body)); - EnsureArg.IsNotNull(seekableStreamConverter, nameof(seekableStreamConverter)); - EnsureArg.IsNotNull(storeConfiguration?.Value, nameof(storeConfiguration)); - - _seekableStreamConverter = seekableStreamConverter; - _storeConfiguration = storeConfiguration; - - if (!MediaTypeHeaderValue.TryParse(contentType, out MediaTypeHeaderValue media) || - !media.MediaType.Equals(KnownContentTypes.MultipartRelated, StringComparison.InvariantCultureIgnoreCase)) - { - throw new UnsupportedMediaTypeException( - string.Format(CultureInfo.InvariantCulture, DicomApiResource.UnsupportedContentType, contentType)); - } - - string boundary = HeaderUtilities.RemoveQuotes(media.Boundary).ToString(); - - if (string.IsNullOrWhiteSpace(boundary)) - { - throw new UnsupportedMediaTypeException( - string.Format(CultureInfo.InvariantCulture, DicomApiResource.InvalidMultipartContentType, contentType)); - } - - // Check to see if the root content type was specified or not. - if (media.Parameters != null) - { - foreach (NameValueHeaderValue parameter in media.Parameters) - { - if (TypeParameterName.Equals(parameter.Name.ToString(), StringComparison.OrdinalIgnoreCase)) - { - _rootContentType = HeaderUtilities.RemoveQuotes(parameter.Value).ToString(); - } - else if (StartParameterName.Equals(parameter.Name.ToString(), StringComparison.OrdinalIgnoreCase)) - { - // TODO: According to RFC2387 3.2, the root section can be specified by using the - // start parameter. For now, we will assume that the first section is the "root" section - // and will add support later. Throw exception in case start is specified. - throw new NotSupportedException(DicomApiResource.StartParameterIsNotSupported); - } - } - } - - _multipartReader = new MultipartReader(boundary, body) - { - // set the max length of each section in bytes - BodyLengthLimit = _storeConfiguration.Value.MaxAllowedDicomFileSize, - }; - } - - /// - public async Task ReadNextBodyPartAsync(CancellationToken cancellationToken) - { - MultipartSection section; - try - { - section = await _multipartReader.ReadNextSectionAsync(cancellationToken); - } - catch (BadHttpRequestException ex) when (ex.Message.StartsWith(BodyTooLargeExceptionMessage, StringComparison.OrdinalIgnoreCase)) - { - throw new PayloadTooLargeException(_storeConfiguration.Value.MaxAllowedDicomFileSize); - } - catch (Exception ex) when (ex is InvalidDataException || - ex is IOException) - { - throw new InvalidMultipartRequestException(ex.Message); - } - - if (section == null) - { - return null; - } - - try - { - string contentType = section.ContentType; - - if (contentType == null && _sectionIndex == 0) - { - // Based on RFC2387 Section 3.1, the content type of the "root" section - // can be specified through the request's Content-Type header. If the content - // type is not specified in the section and this is the "root" section, - // then check to see if it was specified in the request's Content-Type. - contentType = _rootContentType; - } - - _sectionIndex++; - - // The stream must be consumed before the next ReadNextSectionAsync is called. - // Also, the stream returned by the MultipartReader is not seekable. We need to make - // it seekable so that we can process the stream multiple times. - return new MultipartBodyPart( - contentType, - await _seekableStreamConverter.ConvertAsync(section.Body, cancellationToken)); - } - catch (InvalidDataException) - { - // This will result in bad request, we need to handle this differently when we make the processing serial. - throw new PayloadTooLargeException(_storeConfiguration.Value.MaxAllowedDicomFileSize); - } - catch (IOException) - { - // We can terminate here because it seems like after it encounters the IOException, - // next ReadNextSectionAsync will also throws IOException. - return null; - } - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Web/AspNetCoreMultipartReaderFactory.cs b/src/Microsoft.Health.Dicom.Api/Web/AspNetCoreMultipartReaderFactory.cs deleted file mode 100644 index 9f93ee98ca..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Web/AspNetCoreMultipartReaderFactory.cs +++ /dev/null @@ -1,42 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.IO; -using EnsureThat; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Web; - -namespace Microsoft.Health.Dicom.Api.Web; - -/// -/// Provides functionality to create a new instance of . -/// -internal class AspNetCoreMultipartReaderFactory : IMultipartReaderFactory -{ - private readonly ISeekableStreamConverter _seekableStreamConverter; - private readonly IOptions _storeConfiguration; - - public AspNetCoreMultipartReaderFactory( - ISeekableStreamConverter seekableStreamConverter, - IOptions storeConfiguration) - { - EnsureArg.IsNotNull(seekableStreamConverter, nameof(seekableStreamConverter)); - EnsureArg.IsNotNull(storeConfiguration?.Value, nameof(storeConfiguration)); - - _seekableStreamConverter = seekableStreamConverter; - _storeConfiguration = storeConfiguration; - } - - /// - public IMultipartReader Create(string contentType, Stream body) - { - return new AspNetCoreMultipartReader( - contentType, - body, - _seekableStreamConverter, - _storeConfiguration); - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Web/DicomStreamContent.cs b/src/Microsoft.Health.Dicom.Api/Web/DicomStreamContent.cs deleted file mode 100644 index bd88076e82..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Web/DicomStreamContent.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.IO; - -namespace Microsoft.Health.Dicom.Api.Web; - -public class DicomStreamContent : IHttpStreamContent -{ - public Stream Stream { get; init; } - - public long StreamLength { get; init; } - - // could not use HttpContentHeaders since it has no public constructors. HttpHeaders is abstract class - public IEnumerable>> Headers { get; init; } -} diff --git a/src/Microsoft.Health.Dicom.Api/Web/HttpSeekableStreamConverter.cs b/src/Microsoft.Health.Dicom.Api/Web/HttpSeekableStreamConverter.cs deleted file mode 100644 index ad8578096a..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Web/HttpSeekableStreamConverter.cs +++ /dev/null @@ -1,56 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.IO; -using EnsureThat; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Health.Dicom.WebUtilities; - -namespace Microsoft.Health.Dicom.Api.Web; - -public class HttpSeekableStreamConverter : SeekableStreamConverter -{ - private string _tempDirectory; - private readonly IHttpContextAccessor _httpContextAccessor; - - public HttpSeekableStreamConverter(IHttpContextAccessor httpContextAccessor, ILogger logger) - : base(logger) - { - _httpContextAccessor = EnsureArg.IsNotNull(httpContextAccessor, nameof(httpContextAccessor)); - } - - protected override void RegisterForDispose(Stream stream) - { - _httpContextAccessor.HttpContext?.Response.RegisterForDisposeAsync(stream); - } - - protected override long GetContentLength() - { - return _httpContextAccessor.HttpContext?.Request.ContentLength ?? 0; - } - - protected override string GetTempDirectory() - { - if (_tempDirectory != null) - { - return _tempDirectory; - } - - // Look for folders in the following order. - // ASPNETCORE_TEMP - User set temporary location. - string temp = Environment.GetEnvironmentVariable("ASPNETCORE_TEMP") ?? Path.GetTempPath(); - - if (!Directory.Exists(temp)) - { - throw new DirectoryNotFoundException(temp); - } - - _tempDirectory = temp; - - return _tempDirectory; - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Web/IHttpStreamContent.cs b/src/Microsoft.Health.Dicom.Api/Web/IHttpStreamContent.cs deleted file mode 100644 index a0a6909197..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Web/IHttpStreamContent.cs +++ /dev/null @@ -1,16 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.IO; - -namespace Microsoft.Health.Dicom.Api.Web; - -public interface IHttpStreamContent -{ - IEnumerable>> Headers { get; init; } - Stream Stream { get; init; } - long StreamLength { get; init; } -} diff --git a/src/Microsoft.Health.Dicom.Api/Web/InvalidRequestBodyException.cs b/src/Microsoft.Health.Dicom.Api/Web/InvalidRequestBodyException.cs deleted file mode 100644 index 472e859b16..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Web/InvalidRequestBodyException.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Globalization; -using Microsoft.Health.Dicom.Core.Exceptions; - -namespace Microsoft.Health.Dicom.Api.Web; - -public class InvalidRequestBodyException : ValidationException -{ - public InvalidRequestBodyException(string key, string errorMessage) - : base(string.Format(CultureInfo.InvariantCulture, DicomApiResource.InvalidRequestBody, key, errorMessage)) - { - } -} diff --git a/src/Microsoft.Health.Dicom.Api/Web/LazyMultipartReadOnlyStream.cs b/src/Microsoft.Health.Dicom.Api/Web/LazyMultipartReadOnlyStream.cs deleted file mode 100644 index 42b121c35f..0000000000 --- a/src/Microsoft.Health.Dicom.Api/Web/LazyMultipartReadOnlyStream.cs +++ /dev/null @@ -1,214 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Buffers; -using System.Collections.Generic; -using System.IO; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; - -namespace Microsoft.Health.Dicom.Api.Web; - -// inspired by -// https://github.com/microsoft/referencesource/blob/master/System/net/System/Net/Http/MultipartContent.cs -// https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/storage/Azure.Storage.Common/src/Shared/LazyLoadingReadOnlyStream.cs -#pragma warning disable CA1844 // Provide memory-based overrides of async methods when subclassing 'Stream' -public class LazyMultipartReadOnlyStream : Stream -#pragma warning restore CA1844 // Provide memory-based overrides of async methods when subclassing 'Stream' -{ -#pragma warning disable CA2213 // Disposable fields should be disposed, (disposed in ReadAsync) - private readonly IAsyncEnumerator _asyncEnumerator; -#pragma warning restore CA2213 // Disposable fields should be disposed - private readonly int _bufferSize; - private const string Crlf = "\r\n"; - private readonly Encoding _defaultHttpEncoding = Encoding.GetEncoding(28591); - private readonly string _boundary; - private const int KB = 1024; - - private byte[] _buffer; - private IHttpStreamContent _currentStreamContent; - private int _bufferPosition; - private int _bufferLength; - private bool _terminating; - - public LazyMultipartReadOnlyStream( - IAsyncEnumerable enumerableStreams, - string boundary, - int bufferSize, - CancellationToken cancellation) - : base() - { - EnsureArg.IsNotNull(enumerableStreams, nameof(enumerableStreams)); - EnsureArg.IsNotEmptyOrWhiteSpace(boundary, nameof(boundary)); - EnsureArg.IsGt(bufferSize, KB, nameof(bufferSize)); - - _asyncEnumerator = enumerableStreams.GetAsyncEnumerator(cancellation); - _bufferSize = bufferSize; - _buffer = ArrayPool.Shared.Rent(bufferSize); - _boundary = boundary; - } - - public override bool CanRead => true; - - public override bool CanSeek => false; - - public override bool CanWrite => false; - - private string StartBoundary => "--" + _boundary + Crlf; - - private string IntermediateBoundary => Crlf + "--" + _boundary + Crlf; - - private string TerminatingBoundary => Crlf + "--" + _boundary + "--" + Crlf; - - #region NotImplemented Overrides -#pragma warning disable CA1065 // Do not raise exceptions in unexpected locations - public override long Length => throw new NotImplementedException(); -#pragma warning restore CA1065 // Do not raise exceptions in unexpected locations - -#pragma warning disable CA1065 // Do not raise exceptions in unexpected locations - public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } -#pragma warning restore CA1065 // Do not raise exceptions in unexpected locations - - public override void Flush() - { - throw new NotImplementedException(); - } - - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotImplementedException(); - } - - public override void SetLength(long value) - { - throw new NotImplementedException(); - } - - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotImplementedException(); - } - #endregion - - public override int Read(byte[] buffer, int offset, int count) - { - return ReadAsync(buffer, offset, count).GetAwaiter().GetResult(); - } - - public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - ValidateBufferArguments(buffer, offset, count); - - // check - if (_bufferPosition == _bufferLength && _terminating) - { - return 0; - } - - // initialize - if (_currentStreamContent == null) - { - if (await _asyncEnumerator.MoveNextAsync()) - { - _currentStreamContent = _asyncEnumerator.Current; - CopyStringToBuffer(GetHeaderBoundary(true)); - } - else - { - throw new InvalidOperationException("Enumerator cannot be empty"); - } - } - - if (_bufferPosition == _bufferLength && _currentStreamContent.Stream.Position == _currentStreamContent.StreamLength) - { - if (await _asyncEnumerator.MoveNextAsync()) - { - await _currentStreamContent.Stream.DisposeAsync(); - _currentStreamContent = _asyncEnumerator.Current; - CopyStringToBuffer(GetHeaderBoundary(false)); - } - else - { - await _currentStreamContent.Stream.DisposeAsync(); - CopyStringToBuffer(TerminatingBoundary); - _terminating = true; - } - } - - // get - if (!_terminating && _bufferPosition == _bufferLength) - { -#pragma warning disable CA1835 // Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync', (Azure.Storage.LazyLoadingReadOnlyStream does not implement that.) - _bufferLength = await _currentStreamContent.Stream.ReadAsync(_buffer, 0, _bufferSize, cancellationToken); -#pragma warning restore CA1835 // Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync' - _bufferPosition = 0; - } - - // We will return the minimum of remainingBytesInBuffer and the count provided by the user - int remainingBytesInBuffer = _bufferLength - _bufferPosition; - int bytesToWrite = Math.Min(remainingBytesInBuffer, count); - - // copy - Array.Copy(_buffer, _bufferPosition, buffer, offset, bytesToWrite); - _bufferPosition += bytesToWrite; - - return bytesToWrite; - } - - private void CopyStringToBuffer(string multipartHeader) - { - byte[] startBoundary = EncodeStringToByteArray(multipartHeader); - int bytesToWrite = startBoundary.Length; - Array.Copy(startBoundary, 0, _buffer, 0, bytesToWrite); - _bufferPosition = 0; - _bufferLength = bytesToWrite; - } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - // Return the buffer to the pool if we're called from Dispose or a finalizer - if (_buffer != null) - { - ArrayPool.Shared.Return(_buffer, clearArray: true); - _buffer = null; - } - } - - #region MultiPart boundary write - private string GetHeaderBoundary(bool isStart) - { - var multiPartStringBuilder = new StringBuilder(); - if (isStart) - { - multiPartStringBuilder.Append(StartBoundary); - - } - else - { - multiPartStringBuilder.Append(IntermediateBoundary); - } - AppendContentHeader(multiPartStringBuilder, _currentStreamContent.Headers); - return multiPartStringBuilder.ToString(); - } - - private byte[] EncodeStringToByteArray(string input) - { - return _defaultHttpEncoding.GetBytes(input); - } - - private static void AppendContentHeader(StringBuilder builder, IEnumerable>> headers) - { - foreach (KeyValuePair> headerPair in headers) - { - builder.Append(headerPair.Key + ": " + string.Join(", ", headerPair.Value) + Crlf); - } - builder.Append(Crlf); - } - #endregion -} diff --git a/src/Microsoft.Health.Dicom.Azure.UnitTests/KeyVault/KeyVaultSecretStoreTests.cs b/src/Microsoft.Health.Dicom.Azure.UnitTests/KeyVault/KeyVaultSecretStoreTests.cs deleted file mode 100644 index 13f6174e9a..0000000000 --- a/src/Microsoft.Health.Dicom.Azure.UnitTests/KeyVault/KeyVaultSecretStoreTests.cs +++ /dev/null @@ -1,180 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Mime; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using Azure; -using Azure.Security.KeyVault.Secrets; -using Microsoft.Health.Dicom.Azure.KeyVault; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Azure.UnitTests.KeyVault; - -public class KeyVaultSecretStoreTests -{ - private readonly SecretClient _secretClient; - private readonly KeyVaultSecretStore _secretStore; - - public KeyVaultSecretStoreTests() - { - _secretClient = Substitute.For(); - _secretStore = new KeyVaultSecretStore(_secretClient); - } - - [Fact] - public async Task GivenMissingSecret_WhenDeletingSecret_ThenReturnFalse() - { - const string secretName = "MySecret"; - using var tokenSource = new CancellationTokenSource(); - - DeleteSecretOperation operation = Substitute.For(); - _secretClient - .StartDeleteSecretAsync(secretName, tokenSource.Token) - .Returns(Task.FromException( - new RequestFailedException(404, "Not found", "SecretNotFound", null))); - - Assert.False(await _secretStore.DeleteSecretAsync(secretName, tokenSource.Token)); - - await _secretClient.Received(1).StartDeleteSecretAsync(secretName, tokenSource.Token); - await operation.DidNotReceiveWithAnyArgs().WaitForCompletionAsync(default); - } - - [Fact] - public async Task GivenValidSecret_WhenDeletingSecret_ThenReturnTrue() - { - const string secretName = "MySecret"; - using var tokenSource = new CancellationTokenSource(); - - DeleteSecretOperation operation = Substitute.For(); - _secretClient.StartDeleteSecretAsync(secretName, tokenSource.Token).Returns(operation); - operation - .WaitForCompletionAsync(tokenSource.Token) - .Returns(Substitute.For>()); - - Assert.True(await _secretStore.DeleteSecretAsync(secretName, tokenSource.Token)); - - await _secretClient.Received(1).StartDeleteSecretAsync(secretName, tokenSource.Token); - await operation.Received(1).WaitForCompletionAsync(tokenSource.Token); - } - - [Fact] - public async Task GivenMissingSecret_WhenGettingSecret_ThenThrowException() - { - const string secretName = "MySecret", version = "12345"; - using var tokenSource = new CancellationTokenSource(); - - Response response = Substitute.For>(); - _secretClient - .GetSecretAsync(secretName, version, tokenSource.Token) - .Returns(Task.FromException>( - new RequestFailedException(404, "Not found", "SecretNotFound", null))); - - await Assert.ThrowsAsync(() => _secretStore.GetSecretAsync(secretName, version, tokenSource.Token)); - - await _secretClient.Received(1).GetSecretAsync(secretName, version, tokenSource.Token); - } - - [Theory] - [InlineData("Secret1", "12345", "foo")] - [InlineData("Secret2", null, "bar")] - public async Task GivenValidSecret_WhenGettingSecret_ThenGetValue(string name, string version, string value) - { - using var tokenSource = new CancellationTokenSource(); - - Response response = Substitute.For>(); - _secretClient.GetSecretAsync(name, version, tokenSource.Token).Returns(response); - response.Value.Returns(new KeyVaultSecret(name, value)); - - Assert.Equal(value, await _secretStore.GetSecretAsync(name, version, tokenSource.Token)); - - await _secretClient.Received(1).GetSecretAsync(name, version, tokenSource.Token); - } - - [Theory] - [InlineData(null)] - [InlineData("one", "ii", "3")] - public async Task GivenKeyVault_WhenListingSecrets_ThenRetrieveAllNames(params string[] names) - { - names ??= Array.Empty(); - - using var tokenSource = new CancellationTokenSource(); - - AsyncPageable response = Substitute.For>(); - _secretClient.GetPropertiesOfSecretsAsync(tokenSource.Token).Returns(response); - response - .GetAsyncEnumerator(default) // The real implementation forwards the proper token - .Returns(names - .Select(x => new SecretProperties(x)) - .ToAsyncEnumerable() - .GetAsyncEnumerator()); - - string[] actual = await _secretStore.ListSecretsAsync(tokenSource.Token).ToArrayAsync(); - Assert.True(names.SequenceEqual(actual)); - - _secretClient.Received(1).GetPropertiesOfSecretsAsync(tokenSource.Token); - } - - [Theory] - [InlineData("Secret1", "1", "foo")] - [InlineData("Secret1", "2", "bar")] - public async Task GivenKeyVault_WhenSettingSecret_ThenUpdateVersion(string name, string version, string value) - { - using var tokenSource = new CancellationTokenSource(); - - Response response = Substitute.For>(); - _secretClient - .SetSecretAsync(Arg.Is(s => s.Name == name && s.Value == value && s.Properties.ContentType == null), tokenSource.Token) - .Returns(response); - - response.Value.Returns(CreateSecret(name, version, value)); - - Assert.Equal(version, await _secretStore.SetSecretAsync(name, value, tokenSource.Token)); - - await _secretClient - .Received(1) - .SetSecretAsync(Arg.Is(s => s.Name == name && s.Value == value && s.Properties.ContentType == null), tokenSource.Token); - } - - [Theory] - [InlineData("Secret1", "1", "foo", MediaTypeNames.Text.Plain)] - [InlineData("Secret1", "2", "bar", null)] - public async Task GivenKeyVault_WhenSettingSecretWithContentType_ThenUpdateVersion(string name, string version, string value, string contentType) - { - using var tokenSource = new CancellationTokenSource(); - - Response response = Substitute.For>(); - _secretClient - .SetSecretAsync(Arg.Is(s => s.Name == name && s.Value == value), tokenSource.Token) - .Returns(response); - - response.Value.Returns(CreateSecret(name, version, value)); - - Assert.Equal(version, await _secretStore.SetSecretAsync(name, value, contentType, tokenSource.Token)); - - await _secretClient - .Received(1) - .SetSecretAsync(Arg.Is(s => s.Name == name && s.Value == value && s.Properties.ContentType == contentType), tokenSource.Token); - } - - private static KeyVaultSecret CreateSecret(string name, string version, string value) - { - var secret = new KeyVaultSecret(name, value); - - // Version cannot be set via the existing API - MethodInfo setter = typeof(SecretProperties) - .GetProperty(nameof(SecretProperties.Version)) - .GetSetMethod(nonPublic: true); - - setter.Invoke(secret.Properties, new object[] { version }); - - return secret; - } -} diff --git a/src/Microsoft.Health.Dicom.Azure.UnitTests/Microsoft.Health.Dicom.Azure.UnitTests.csproj b/src/Microsoft.Health.Dicom.Azure.UnitTests/Microsoft.Health.Dicom.Azure.UnitTests.csproj deleted file mode 100644 index 372eb358d4..0000000000 --- a/src/Microsoft.Health.Dicom.Azure.UnitTests/Microsoft.Health.Dicom.Azure.UnitTests.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - $(LibraryFrameworks) - - - - - - - - - - - - - - - - - diff --git a/src/Microsoft.Health.Dicom.Azure/DicomAzureResource.Designer.cs b/src/Microsoft.Health.Dicom.Azure/DicomAzureResource.Designer.cs deleted file mode 100644 index b90a89c873..0000000000 --- a/src/Microsoft.Health.Dicom.Azure/DicomAzureResource.Designer.cs +++ /dev/null @@ -1,72 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Microsoft.Health.Dicom.Azure { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class DicomAzureResource { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal DicomAzureResource() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.Health.Dicom.Azure.DicomAzureResource", typeof(DicomAzureResource).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to Cannot find a secret with the name '{0}' and version '{1}'.. - /// - internal static string SecretNotFound { - get { - return ResourceManager.GetString("SecretNotFound", resourceCulture); - } - } - } -} diff --git a/src/Microsoft.Health.Dicom.Azure/DicomAzureResource.resx b/src/Microsoft.Health.Dicom.Azure/DicomAzureResource.resx deleted file mode 100644 index facd4ea17d..0000000000 --- a/src/Microsoft.Health.Dicom.Azure/DicomAzureResource.resx +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Cannot find a secret with the name '{0}' and version '{1}'. - {0} Secret name. {1} Version - - \ No newline at end of file diff --git a/src/Microsoft.Health.Dicom.Azure/KeyVault/KeyVaultSecretStore.cs b/src/Microsoft.Health.Dicom.Azure/KeyVault/KeyVaultSecretStore.cs deleted file mode 100644 index dd8d3e5a1e..0000000000 --- a/src/Microsoft.Health.Dicom.Azure/KeyVault/KeyVaultSecretStore.cs +++ /dev/null @@ -1,74 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Azure; -using Azure.Security.KeyVault.Secrets; -using EnsureThat; -using Microsoft.Health.Dicom.Core.Features.Common; - -namespace Microsoft.Health.Dicom.Azure.KeyVault; - -internal sealed class KeyVaultSecretStore : ISecretStore -{ - private readonly SecretClient _secretClient; - - private const string SecretNotFoundErrorCode = "SecretNotFound"; - - public KeyVaultSecretStore(SecretClient secretClient) - => _secretClient = EnsureArg.IsNotNull(secretClient, nameof(secretClient)); - - public async Task DeleteSecretAsync(string name, CancellationToken cancellationToken = default) - { - DeleteSecretOperation operation; - try - { - operation = await _secretClient.StartDeleteSecretAsync(name, cancellationToken); - } - catch (RequestFailedException rfe) when (rfe.ErrorCode == SecretNotFoundErrorCode) - { - return false; - } - - await operation.WaitForCompletionAsync(cancellationToken); - return true; - } - - public async Task GetSecretAsync(string name, string version = null, CancellationToken cancellationToken = default) - { - try - { - Response response = await _secretClient.GetSecretAsync(name, version, cancellationToken); - return response.Value.Value; - } - catch (RequestFailedException rfe) when (rfe.ErrorCode == SecretNotFoundErrorCode) - { - throw new KeyNotFoundException( - string.Format(CultureInfo.CurrentCulture, DicomAzureResource.SecretNotFound, name, version), - rfe); - } - } - - public IAsyncEnumerable ListSecretsAsync(CancellationToken cancellationToken = default) - => _secretClient.GetPropertiesOfSecretsAsync(cancellationToken).Select(x => x.Name); - - public Task SetSecretAsync(string name, string value, CancellationToken cancellationToken = default) - => SetSecretAsync(name, value, null, cancellationToken); - - public async Task SetSecretAsync(string name, string value, string contentType, CancellationToken cancellationToken = default) - { - var secret = new KeyVaultSecret(name, value); - - if (contentType != null) - secret.Properties.ContentType = contentType; - - Response response = await _secretClient.SetSecretAsync(secret, cancellationToken); - return response.Value.Properties.Version; - } -} diff --git a/src/Microsoft.Health.Dicom.Azure/KeyVaultSecretClientOptions.cs b/src/Microsoft.Health.Dicom.Azure/KeyVaultSecretClientOptions.cs deleted file mode 100644 index 8b783a232e..0000000000 --- a/src/Microsoft.Health.Dicom.Azure/KeyVaultSecretClientOptions.cs +++ /dev/null @@ -1,31 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using Azure.Security.KeyVault.Secrets; - -namespace Microsoft.Health.Dicom.Azure; - -internal sealed class KeyVaultSecretClientOptions : SecretClientOptions -{ - public const string SectionName = "KeyVault"; - - // This value is based on the SecretClient's parameter name and is read automatically from the configuration - public Uri VaultUri { get; set; } - - // TODO: Replace usage of Endpoint with VaultUri - [Obsolete($"Please use {nameof(VaultUri)} instead.")] - public Uri Endpoint - { - get => _endpoint; - set - { - _endpoint = value; - VaultUri = value; - } - } - - private Uri _endpoint; -} diff --git a/src/Microsoft.Health.Dicom.Azure/Microsoft.Health.Dicom.Azure.csproj b/src/Microsoft.Health.Dicom.Azure/Microsoft.Health.Dicom.Azure.csproj deleted file mode 100644 index 6ba3186469..0000000000 --- a/src/Microsoft.Health.Dicom.Azure/Microsoft.Health.Dicom.Azure.csproj +++ /dev/null @@ -1,38 +0,0 @@ - - - - Implementation of Azure dependencies used in DICOM core library - $(LibraryFrameworks) - - - - - - - - - - - - - - - - - - - - True - True - DicomAzureResource.resx - - - - - - ResXFileCodeGenerator - DicomAzureResource.Designer.cs - - - - diff --git a/src/Microsoft.Health.Dicom.Azure/Properties/AssemblyInfo.cs b/src/Microsoft.Health.Dicom.Azure/Properties/AssemblyInfo.cs deleted file mode 100644 index 763f844318..0000000000 --- a/src/Microsoft.Health.Dicom.Azure/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,12 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Resources; -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("Microsoft.Health.Dicom.Azure.UnitTests")] -[assembly: NeutralResourcesLanguage("en-us")] -[assembly: CLSCompliant(false)] diff --git a/src/Microsoft.Health.Dicom.Azure/Registration/KeyVaultClientRegistrationExtensions.cs b/src/Microsoft.Health.Dicom.Azure/Registration/KeyVaultClientRegistrationExtensions.cs deleted file mode 100644 index cd4269baf7..0000000000 --- a/src/Microsoft.Health.Dicom.Azure/Registration/KeyVaultClientRegistrationExtensions.cs +++ /dev/null @@ -1,108 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using Azure.Core.Extensions; -using Azure.Security.KeyVault.Secrets; -using EnsureThat; -using Microsoft.Extensions.Azure; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Health.Core.Extensions; -using Microsoft.Health.Dicom.Azure; -using Microsoft.Health.Dicom.Azure.KeyVault; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Registration; - -namespace Microsoft.Extensions.DependencyInjection; - -/// -/// A collection of methods for registering Azure Key Vault clients. -/// -public static class KeyVaultClientRegistrationExtensions -{ - /// - /// Adds a secret client for Azure Key Vault. - /// - /// The DICOM server builder instance. - /// The configuration for the client. - /// Optional action for configuring the options. - /// The server builder. - /// - /// or is . - /// - public static IDicomServerBuilder AddKeyVaultClient( - this IDicomServerBuilder builder, - IConfiguration configuration, - Action configureOptions = null) - { - EnsureArg.IsNotNull(builder, nameof(builder)); - EnsureArg.IsNotNull(configuration, nameof(configuration)); - - builder.Services.AddKeyVaultClient(configuration, configureOptions); - return builder; - } - - /// - /// Adds a secret client for Azure Key Vault. - /// - /// The DICOM functions builder instance. - /// The configuration for the client. - /// Optional action for configuring the options. - /// The server builder. - /// - /// or is . - /// - public static IDicomFunctionsBuilder AddKeyVaultClient( - this IDicomFunctionsBuilder builder, - IConfiguration configuration, - Action configureOptions = null) - { - EnsureArg.IsNotNull(builder, nameof(builder)); - EnsureArg.IsNotNull(configuration, nameof(configuration)); - - builder.Services.AddKeyVaultClient(configuration, configureOptions); - return builder; - } - - private static IServiceCollection AddKeyVaultClient( - this IServiceCollection services, - IConfiguration configuration, - Action configureOptions) - { - EnsureArg.IsNotNull(services, nameof(services)); - EnsureArg.IsNotNull(configuration, nameof(configuration)); - - IConfigurationSection section = configuration.GetSection(KeyVaultSecretClientOptions.SectionName); - - var options = new KeyVaultSecretClientOptions(); - section.Bind(options); - configureOptions?.Invoke(options); - - // Note: We can disable key vault in local development scenarios, like F5 or Docker - if (options.VaultUri != null) - { - // Backfill from obsolete setting -#pragma warning disable CS0618 - if (options.Endpoint != null) - section[nameof(KeyVaultSecretClientOptions.VaultUri)] = section[nameof(KeyVaultSecretClientOptions.Endpoint)]; -#pragma warning restore CS0618 - - services.AddAzureClients(builder => - { - IAzureClientBuilder clientBuilder = builder - .AddSecretClient(section) - .WithRetryableCredential(section); - - if (configureOptions != null) - clientBuilder.ConfigureOptions(configureOptions); - }); - - services.AddScoped(); - } - - return services; - } -} diff --git a/src/Microsoft.Health.Dicom.Blob.UnitTests/DicomFileNameWithPrefixTests.cs b/src/Microsoft.Health.Dicom.Blob.UnitTests/DicomFileNameWithPrefixTests.cs deleted file mode 100644 index 5b47279082..0000000000 --- a/src/Microsoft.Health.Dicom.Blob.UnitTests/DicomFileNameWithPrefixTests.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Microsoft.Health.Dicom.Blob.Features.Storage; -using Microsoft.Health.Dicom.Blob.Utilities; -using Xunit; - -namespace Microsoft.Health.Dicom.Blob.UnitTests.Features.Common; - -public class DicomFileNameWithPrefixTests -{ - private readonly DicomFileNameWithPrefix _nameWithPrefix; - - public DicomFileNameWithPrefixTests() - { - _nameWithPrefix = new DicomFileNameWithPrefix(); - } - - [Fact] - public void GivenIdentifier_GetFileNames_ShouldReturnExpectedValues() - { - var version = 1; - Assert.Equal($"{HashingHelper.ComputeXXHash(version, 3)}_{version}.dcm", _nameWithPrefix.GetInstanceFileName(version)); - Assert.Equal($"{HashingHelper.ComputeXXHash(version, 3)}_{version}_metadata.json", _nameWithPrefix.GetMetadataFileName(version)); - Assert.Equal($"{HashingHelper.ComputeXXHash(version, 3)}_{version}_frames_range.json", _nameWithPrefix.GetInstanceFramesRangeFileName(version)); - Assert.Equal($"{HashingHelper.ComputeXXHash(version, 3)}_ {version}_frames_range.json", _nameWithPrefix.GetInstanceFramesRangeFileNameWithSpace(version)); - } -} diff --git a/src/Microsoft.Health.Dicom.Blob.UnitTests/Features/Export/AzureBlobExportSinkProviderTests.cs b/src/Microsoft.Health.Dicom.Blob.UnitTests/Features/Export/AzureBlobExportSinkProviderTests.cs deleted file mode 100644 index b3e18af550..0000000000 --- a/src/Microsoft.Health.Dicom.Blob.UnitTests/Features/Export/AzureBlobExportSinkProviderTests.cs +++ /dev/null @@ -1,334 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.ComponentModel.DataAnnotations; -using System.Diagnostics.CodeAnalysis; -using System.Net.Mime; -using System.Text.Encodings.Web; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Azure.Identity; -using Azure.Storage; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using Microsoft.Health.Blob.Configs; -using Microsoft.Health.Core.Features.Identity; -using Microsoft.Health.Dicom.Blob.Features.Export; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Export; -using Microsoft.Health.Dicom.Core.Models; -using Microsoft.Health.Dicom.Core.Models.Export; -using Microsoft.Health.Operations; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Blob.UnitTests.Features.Export; - -public class AzureBlobExportSinkProviderTests -{ - private readonly ISecretStore _secretStore; - private readonly IFileStore _fileStore; - private readonly IExternalCredentialProvider _credentialProvider; - private readonly AzureBlobExportSinkProviderOptions _providerOptions; - private readonly AzureBlobClientOptions _clientOptions; - private readonly BlobOperationOptions _operationOptions; - private readonly JsonSerializerOptions _serializerOptions; - private readonly AzureBlobExportSinkProvider _sinkProvider; - private readonly AzureBlobExportSinkProvider _secretlessSinkProvider; - - public AzureBlobExportSinkProviderTests() - { - _secretStore = Substitute.For(); - _fileStore = Substitute.For(); - _credentialProvider = Substitute.For(); - _providerOptions = new AzureBlobExportSinkProviderOptions { AllowPublicAccess = true, AllowSasTokens = true }; - _clientOptions = new AzureBlobClientOptions(); - _operationOptions = new BlobOperationOptions { Upload = new StorageTransferOptions() }; - _serializerOptions = new JsonSerializerOptions(); - _serializerOptions.ConfigureDefaultDicomSettings(); - - _sinkProvider = new AzureBlobExportSinkProvider( - _secretStore, - _fileStore, - _credentialProvider, - CreateSnapshot(_providerOptions), - CreateSnapshot(_clientOptions, AzureBlobExportSinkProvider.ClientOptionsName), - CreateSnapshot(_operationOptions), - CreateSnapshot(_serializerOptions), - NullLogger.Instance); - - _secretlessSinkProvider = new AzureBlobExportSinkProvider( - _fileStore, - _credentialProvider, - CreateSnapshot(_providerOptions), - CreateSnapshot(_clientOptions, AzureBlobExportSinkProvider.ClientOptionsName), - CreateSnapshot(_operationOptions), - CreateSnapshot(_serializerOptions), - NullLogger.Instance); - } - - [Theory] - [InlineData(false, false, false)] - [InlineData(false, true, false)] - [InlineData(true, false, false)] - [InlineData(true, true, true)] - public async Task GivenCompletedOperation_WhenCleaningUp_SkipIfNoWork(bool enableSecrets, bool addSecret, bool expectDelete) - { - using var tokenSource = new CancellationTokenSource(); - - AzureBlobExportSinkProvider provider = enableSecrets ? _sinkProvider : _secretlessSinkProvider; - var options = new AzureBlobExportOptions - { - Secret = addSecret ? new SecretKey { Name = "secret" } : null - }; - - await provider.CompleteCopyAsync(options, tokenSource.Token); - - if (expectDelete) - await _secretStore.Received(1).DeleteSecretAsync("secret", tokenSource.Token); - else - await _secretStore.DidNotReceiveWithAnyArgs().DeleteSecretAsync(default, default); - } - - [Fact] - public async Task GivenNoSecretStore_WhenCreatingSinkWithSecret_ThenThrow() - { - var containerUri = new Uri("https://unit-test.blob.core.windows.net/mycontainer?sv=2020-08-04&ss=b", UriKind.Absolute); - var errorHref = new Uri($"https://unit-test.blob.core.windows.net/mycontainer/{Guid.NewGuid()}/errors.log", UriKind.Absolute); - var options = new AzureBlobExportOptions - { - Secret = new SecretKey { Name = "foo", Version = "bar" }, - }; - - await Assert.ThrowsAsync(() => _secretlessSinkProvider.CreateAsync(options, Guid.NewGuid())); - await _secretStore.DidNotReceiveWithAnyArgs().GetSecretAsync(default, default, default); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task GivenBlobContainerUri_WhenCreatingSink_ThenCreateFromServiceContainer(bool useManagedIdentity) - { - const string version = "1"; - var operationId = Guid.NewGuid(); - var containerUri = new Uri("https://unit-test.blob.core.windows.net/mycontainer?sv=2020-08-04&ss=b", UriKind.Absolute); - var options = new AzureBlobExportOptions - { - Secret = new SecretKey - { - Name = operationId.ToString(OperationId.FormatSpecifier), - Version = version, - }, - UseManagedIdentity = useManagedIdentity, - }; - - using var tokenSource = new CancellationTokenSource(); - - _secretStore - .GetSecretAsync(operationId.ToString(OperationId.FormatSpecifier), version, tokenSource.Token) - .Returns(GetJson(containerUri)); - - if (useManagedIdentity) - _credentialProvider.GetTokenCredential().Returns(new DefaultAzureCredential()); - - IExportSink sink = await _sinkProvider.CreateAsync(options, operationId, tokenSource.Token); - - Assert.IsType(sink); - await _secretStore - .Received(1) - .GetSecretAsync(operationId.ToString(OperationId.FormatSpecifier), version, tokenSource.Token); - - if (useManagedIdentity) - _credentialProvider.Received(1).GetTokenCredential(); - else - _credentialProvider.DidNotReceiveWithAnyArgs().GetTokenCredential(); - } - - [Fact] - public async Task GivenConnectionString_WhenCreatingSink_ThenCreateFromServiceContainer() - { - const string version = "1"; - var operationId = Guid.NewGuid(); - var connectionString = "BlobEndpoint=https://unit-test.blob.core.windows.net/;SharedAccessSignature=sastoken"; - var options = new AzureBlobExportOptions - { - BlobContainerName = "mycontainer", - Secret = new SecretKey - { - Name = operationId.ToString(OperationId.FormatSpecifier), - Version = version, - }, - }; - - using var tokenSource = new CancellationTokenSource(); - - _secretStore - .GetSecretAsync(operationId.ToString(OperationId.FormatSpecifier), version, tokenSource.Token) - .Returns(GetJson(connectionString)); - - IExportSink sink = await _sinkProvider.CreateAsync(options, operationId, tokenSource.Token); - - Assert.IsType(sink); - await _secretStore - .Received(1) - .GetSecretAsync(operationId.ToString(OperationId.FormatSpecifier), version, tokenSource.Token); - } - - [Theory] - [InlineData("BlobEndpoint=https://unit-test.blob.core.windows.net/;SharedAccessSignature=sastoken", "export-e2e-test", null)] - [InlineData(null, null, "https://dcmcipermanpmlxszw4sayty.blob.core.windows.net/export-e2e-test?sig=sastoken")] - [SuppressMessage("Design", "CA1054:URI-like parameters should not be strings", Justification = "Cannot use inline Uri")] - public async Task GivenSasTokenWithNoSecretStore_WhenSecuringInfo_ThenThrow(string connectionString, string blobContainerName, string blobContainerUri) - { - var operationId = Guid.NewGuid(); - var options = new AzureBlobExportOptions - { - BlobContainerName = blobContainerName, - BlobContainerUri = blobContainerUri != null ? new Uri(blobContainerUri, UriKind.Absolute) : null, - ConnectionString = connectionString, - }; - - await Assert.ThrowsAsync(() => _secretlessSinkProvider.SecureSensitiveInfoAsync(options, operationId)); - await _secretStore.DidNotReceiveWithAnyArgs().SetSecretAsync(default, default, default, default); - } - - [Theory] - [InlineData("BlobEndpoint=https://unit-test.blob.core.windows.net/;", "export-e2e-test", null)] - [InlineData(null, null, "https://dcmcipermanpmlxszw4sayty.blob.core.windows.net/export-e2e-test")] - [SuppressMessage("Design", "CA1054:URI-like parameters should not be strings", Justification = "Cannot use inline Uri")] - public async Task GivenNoSasToken_WhenSecuringInfo_ThenSkip(string connectionString, string blobContainerName, string blobContainerUri) - { - var options = new AzureBlobExportOptions - { - BlobContainerName = blobContainerName, - BlobContainerUri = blobContainerUri != null ? new Uri(blobContainerUri, UriKind.Absolute) : null, - ConnectionString = connectionString, - }; - - var actual = (AzureBlobExportOptions)await _sinkProvider.SecureSensitiveInfoAsync(options, Guid.NewGuid()); - - await _secretStore.DidNotReceiveWithAnyArgs().SetSecretAsync(default, default, default, default); - - Assert.Null(actual.Secret); - Assert.Equal(blobContainerName, actual.BlobContainerName); - Assert.Equal(blobContainerUri, actual.BlobContainerUri?.AbsoluteUri); - Assert.Equal(connectionString, actual.ConnectionString); - } - - [Theory] - [InlineData("BlobEndpoint=https://unit-test.blob.core.windows.net/;SharedAccessSignature=sastoken", "export-e2e-test", null)] - [InlineData(null, null, "https://dcmcipermanpmlxszw4sayty.blob.core.windows.net/export-e2e-test?sig=sastoken")] - [SuppressMessage("Design", "CA1054:URI-like parameters should not be strings", Justification = "Cannot use inline Uri")] - public async Task GivenSasToken_WhenSecuringInfo_ThenStoreSecrets(string connectionString, string blobContainerName, string blobContainerUri) - { - const string version = "1"; - var operationId = Guid.NewGuid(); - var options = new AzureBlobExportOptions - { - BlobContainerName = blobContainerName, - BlobContainerUri = blobContainerUri != null ? new Uri(blobContainerUri, UriKind.Absolute) : null, - ConnectionString = connectionString, - }; - string secretJson = blobContainerUri != null ? GetJson(options.BlobContainerUri) : GetJson(connectionString); - - using var tokenSource = new CancellationTokenSource(); - - _secretStore - .SetSecretAsync(operationId.ToString(OperationId.FormatSpecifier), secretJson, MediaTypeNames.Application.Json, tokenSource.Token) - .Returns(version); - - var actual = (AzureBlobExportOptions)await _sinkProvider.SecureSensitiveInfoAsync(options, operationId, tokenSource.Token); - - await _secretStore - .Received(1) - .SetSecretAsync(operationId.ToString(OperationId.FormatSpecifier), secretJson, MediaTypeNames.Application.Json, tokenSource.Token); - - Assert.Equal(operationId.ToString(OperationId.FormatSpecifier), actual.Secret.Name); - Assert.Equal(version, actual.Secret.Version); - Assert.Null(options.BlobContainerUri); - Assert.Null(options.ConnectionString); - } - - [Theory] - [InlineData(null, " ", "mycontainer", false)] - [InlineData(null, "BlobEndpoint=https://unit-test.blob.core.windows.net/;Foo=Bar", null, false)] - [InlineData("https://unit-test.blob.core.windows.net/mycontainer", "BlobEndpoint=https://unit-test.blob.core.windows.net/;Foo=Bar", null, false)] - [InlineData("https://unit-test.blob.core.windows.net/mycontainer", null, "mycontainer", false)] - [InlineData(null, "BlobEndpoint=https://unit-test.blob.core.windows.net/;AccountKey=abc123", "mycontainer", false)] - [InlineData(null, "BlobEndpoint=https://unit-test.blob.core.windows.net/;SharedAccessSignature=abc123", "mycontainer", true)] - [InlineData("https://unit-test.blob.core.windows.net/mycontainer?sig=foo", null, null, true)] - [InlineData("https://unit-test.blob.core.windows.net/mycontainer?foo=bar&sig=baz", null, null, true)] - [SuppressMessage("Design", "CA1054:URI-like parameters should not be strings", Justification = "URIs cannot be used inline.")] - public async Task GivenInvalidOptions_WhenValidating_ThrowValidationException( - string blobContainerUri, - string connectionString, - string blobContainerName, - bool useManagedIdentity) - { - var options = new AzureBlobExportOptions - { - ConnectionString = connectionString, - BlobContainerName = blobContainerName, - BlobContainerUri = blobContainerUri != null ? new Uri(blobContainerUri, UriKind.Absolute) : null, - UseManagedIdentity = useManagedIdentity, - }; - - await Assert.ThrowsAsync(() => _sinkProvider.ValidateAsync(options)); - } - - [Theory] - [InlineData("BlobEndpoint=https://unit-test.blob.core.windows.net/", "export-e2e-test", null)] - [InlineData(null, null, "https://dcmcipermanpmlxszw4sayty.blob.core.windows.net/export-e2e-test")] - [SuppressMessage("Design", "CA1054:URI-like parameters should not be strings", Justification = "Cannot use inline Uri")] - public async Task GivenPublicAccess_WhenDisallowed_ThrowValidationException(string connectionString, string blobContainerName, string blobContainerUri) - { - var options = new AzureBlobExportOptions - { - BlobContainerName = blobContainerName, - BlobContainerUri = blobContainerUri != null ? new Uri(blobContainerUri, UriKind.Absolute) : null, - ConnectionString = connectionString, - UseManagedIdentity = false, // Be explicit - }; - - _providerOptions.AllowPublicAccess = false; - await Assert.ThrowsAsync(() => _sinkProvider.ValidateAsync(options)); - } - - [Theory] - [InlineData("BlobEndpoint=https://unit-test.blob.core.windows.net/;SharedAccessSignature=sastoken", "export-e2e-test", null)] - [InlineData(null, null, "https://dcmcipermanpmlxszw4sayty.blob.core.windows.net/export-e2e-test?sig=sastoken")] - [SuppressMessage("Design", "CA1054:URI-like parameters should not be strings", Justification = "Cannot use inline Uri")] - public async Task GivenSasToken_WhenDisallowed_ThrowValidationException(string connectionString, string blobContainerName, string blobContainerUri) - { - var options = new AzureBlobExportOptions - { - BlobContainerName = blobContainerName, - BlobContainerUri = blobContainerUri != null ? new Uri(blobContainerUri, UriKind.Absolute) : null, - ConnectionString = connectionString, - }; - - _providerOptions.AllowSasTokens = false; - await Assert.ThrowsAsync(() => _sinkProvider.ValidateAsync(options)); - } - - private static IOptionsSnapshot CreateSnapshot(T options, string name = "") where T : class - { - IOptionsSnapshot snapshot = Substitute.For>(); - snapshot.Get(name).Returns(options); - if (name == "") - snapshot.Value.Returns(options); - - return snapshot; - } - - private static string GetJson(Uri blobContainerUri) - => $"{{\"blobContainerUri\":\"{JavaScriptEncoder.Default.Encode(blobContainerUri.AbsoluteUri)}\"}}"; - - private static string GetJson(string connectionString) - => $"{{\"connectionString\":\"{JavaScriptEncoder.Default.Encode(connectionString)}\"}}"; -} diff --git a/src/Microsoft.Health.Dicom.Blob.UnitTests/Features/Export/AzureBlobExportSinkTests.cs b/src/Microsoft.Health.Dicom.Blob.UnitTests/Features/Export/AzureBlobExportSinkTests.cs deleted file mode 100644 index ead8ab9d1f..0000000000 --- a/src/Microsoft.Health.Dicom.Blob.UnitTests/Features/Export/AzureBlobExportSinkTests.cs +++ /dev/null @@ -1,390 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Text; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Azure; -using Azure.Identity; -using Azure.Storage; -using Azure.Storage.Blobs; -using Azure.Storage.Blobs.Models; -using Azure.Storage.Blobs.Specialized; -using Microsoft.Health.Blob.Configs; -using Microsoft.Health.Dicom.Blob.Features.Export; -using Microsoft.Health.Dicom.Core; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Export; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Models.Common; -using Microsoft.Health.Dicom.Core.Models.Export; -using Microsoft.Health.Dicom.Core.Serialization; -using NSubstitute; -using NSubstitute.ExceptionExtensions; -using Xunit; - -namespace Microsoft.Health.Dicom.Blob.UnitTests.Features.Export; - -public class AzureBlobExportSinkTests : IAsyncDisposable -{ - private readonly IFileStore _fileStore; - private readonly BlobContainerClient _destClient; - private readonly BlobClient _destBlob; - private readonly AppendBlobClient _errorBlob; - private readonly MemoryStream _errorStream; - private readonly AzureBlobExportFormatOptions _output; - private readonly BlobOperationOptions _blobOptions; - private readonly JsonSerializerOptions _jsonOptions; - private readonly AzureBlobExportSink _sink; - - public AzureBlobExportSinkTests() - { - var operationId = Guid.NewGuid(); - - _fileStore = Substitute.For(); - _destClient = Substitute.For(); - _destClient.Uri.Returns(new Uri("https://unit-test.blob.core.windows.net/mycontainer?sv=2020-08-04&ss=b", UriKind.Absolute)); - _destBlob = Substitute.For(); - _errorStream = new MemoryStream(); - _errorBlob = Substitute.For(); - _errorBlob - .AppendBlockAsync(Arg.Any(), Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(Substitute.For>())) - .AndDoes(c => - { - object[] args = c.Args(); - var input = args[0] as Stream; - input.CopyTo(_errorStream); - }); - _output = new AzureBlobExportFormatOptions( - operationId, - "%Operation%/results/%Study%/%Series%/%SopInstance%.dcm", - "%Operation%/errors.log"); - _destClient - .GetAppendBlobClient($"{_output.OperationId:N}/errors.log") - .Returns(_errorBlob); - _blobOptions = new BlobOperationOptions - { - Upload = new StorageTransferOptions - { - MaximumConcurrency = 1, - }, - }; - _jsonOptions = new JsonSerializerOptions(); - _jsonOptions.Converters.Add(new DicomIdentifierJsonConverter()); - _sink = new AzureBlobExportSink(_fileStore, _destClient, _output, _blobOptions, _jsonOptions); - } - - [Fact] - public async Task GivenValidReadResult_WhenCopying_ThenCopyToDestination() - { - var instance = new InstanceMetadata( - new VersionedInstanceIdentifier("1.2", "3.4.5", "6.7.8.9.10", 1), - new InstanceProperties()); - - using var fileStream = new MemoryStream(); - using var tokenSource = new CancellationTokenSource(); - - _fileStore.GetStreamingFileAsync( - instance.VersionedInstanceIdentifier.Version, - instance.VersionedInstanceIdentifier.Partition, - fileProperties: null, - cancellationToken: tokenSource.Token) - .Returns(fileStream); - _destClient.GetBlobClient($"{_output.OperationId:N}/results/1.2/3.4.5/6.7.8.9.10.dcm").Returns(_destBlob); - _destBlob - .UploadAsync(fileStream, Arg.Is(x => x.TransferOptions == _blobOptions.Upload), tokenSource.Token) - .Returns(Task.FromResult(Substitute.For>())); - - Assert.True(await _sink.CopyAsync(ReadResult.ForInstance(instance), tokenSource.Token)); - - await _fileStore.Received(1).GetStreamingFileAsync( - instance.VersionedInstanceIdentifier.Version, - instance.VersionedInstanceIdentifier.Partition, - fileProperties: null, - cancellationToken: tokenSource.Token); - _destClient.Received(1).GetBlobClient($"{_output.OperationId:N}/results/1.2/3.4.5/6.7.8.9.10.dcm"); - await _destBlob - .Received(1) - .UploadAsync(fileStream, Arg.Is(x => x.TransferOptions == _blobOptions.Upload), tokenSource.Token); - await _errorBlob - .DidNotReceiveWithAnyArgs() - .AppendBlockAsync(default, default, default); - } - - [Fact] - public async Task GivenInvalidReadResult_WhenCopying_ThenWriteToErrorLog() - { - var failure = new ReadFailureEventArgs(DicomIdentifier.ForSeries("1.2.3", "4.5"), new FileNotFoundException("Cannot find series.")); - using var tokenSource = new CancellationTokenSource(); - - Assert.False(await _sink.CopyAsync(ReadResult.ForFailure(failure), tokenSource.Token)); - - await _fileStore.DidNotReceiveWithAnyArgs().GetStreamingFileAsync(default, default, default); - _destClient.DidNotReceiveWithAnyArgs().GetBlobClient(default); - await _destBlob.DidNotReceiveWithAnyArgs().UploadAsync(default(Stream), default(BlobUploadOptions), default); - - // Extension method appears to prevent the match - // _destClient.Received(1).GetAppendBlobClient($"{_output.OperationId:N}/Errors.json"); - - // Check errors - ExportErrorLogEntry error = await GetErrorsAsync(tokenSource.Token).SingleAsync(); - Assert.Equal(DicomIdentifier.ForSeries("1.2.3", "4.5"), error.Identifier); - Assert.Equal("Cannot find series.", error.Error); - } - - [Fact] - public async Task GivenGetStreamingFileAsyncFailureOnConditionNotMet_WhenCopyingFromIDPExternalStore_ThenWriteToErrorLog() - { - var instance = new InstanceMetadata( - new VersionedInstanceIdentifier("1.2", "3.4.5", "6.7.8.9.10", 1), - new InstanceProperties - { - FileProperties = new FileProperties - { - Path = "partition1/123.dcm", - ETag = "e456", - ContentLength = 123 - } - }); - using var fileStream = new MemoryStream(); - using var tokenSource = new CancellationTokenSource(); - - // when using external store, it is possible to have had the file we are attempting to export change since we've indexed it - // in that case, its etag will be different from file properties we passed in and the etag matching condition error will be thrown - - DataStoreRequestFailedException expectedException = new DataStoreRequestFailedException( - new RequestFailedException( - status: 412, - message: "Condition was not met.", - errorCode: BlobErrorCode.ConditionNotMet.ToString(), - innerException: new Exception("Condition not met.")), - isExternal: true); - - _fileStore.GetStreamingFileAsync( - instance.VersionedInstanceIdentifier.Version, - instance.VersionedInstanceIdentifier.Partition, - instance.InstanceProperties.FileProperties, - cancellationToken: tokenSource.Token) - .Throws(expectedException); - - Assert.False(await _sink.CopyAsync(ReadResult.ForInstance(instance), tokenSource.Token)); - - await _fileStore.Received(1).GetStreamingFileAsync(instance.VersionedInstanceIdentifier.Version, instance.VersionedInstanceIdentifier.Partition, fileProperties: instance.InstanceProperties.FileProperties, cancellationToken: tokenSource.Token); - _destClient.DidNotReceiveWithAnyArgs().GetBlobClient(Arg.Any()); - await _destBlob - .DidNotReceiveWithAnyArgs() - .UploadAsync(Arg.Any(), Arg.Any(), Arg.Any()); - - // Check errors - ExportErrorLogEntry error = await GetErrorsAsync(tokenSource.Token).SingleAsync(); - Assert.Equal(DicomIdentifier.ForInstance("1.2", "3.4.5", "6.7.8.9.10"), error.Identifier); - Assert.Equal(string.Format(CultureInfo.InvariantCulture, DicomCoreResource.ExternalDataStoreOperationFailed, BlobErrorCode.ConditionNotMet.ToString()), error.Error); - } - - [Fact] - public async Task GivenGetStreamingFileAsyncFailureOnNoAccess_WhenCopyingFromIDPExternalStore_ThenExpectExceptionThrown() - { - var instance = new InstanceMetadata( - new VersionedInstanceIdentifier("1.2", "3.4.5", "6.7.8.9.10", 1), - new InstanceProperties - { - FileProperties = new FileProperties - { - Path = "partition1/123.dcm", - ETag = "e456", - ContentLength = 123 - } - }); - using var fileStream = new MemoryStream(); - using var tokenSource = new CancellationTokenSource(); - - // when using external store, it is possible that we do not have access or some other issue occurs, resulting in - // DataStoreException, for which we want to fail the operation and allow retries - - string expectedMessage = "Simulated failure."; - DataStoreException expectedException = new DataStoreException(expectedMessage, isExternal: true); - - _fileStore.GetStreamingFileAsync( - instance.VersionedInstanceIdentifier.Version, - instance.VersionedInstanceIdentifier.Partition, - instance.InstanceProperties.FileProperties, - cancellationToken: tokenSource.Token) - .Throws(expectedException); - - DataStoreException thrownException = await Assert.ThrowsAsync(async () => await _sink.CopyAsync(ReadResult.ForInstance(instance), tokenSource.Token)); - - await _fileStore.Received(1).GetStreamingFileAsync(instance.VersionedInstanceIdentifier.Version, instance.VersionedInstanceIdentifier.Partition, fileProperties: instance.InstanceProperties.FileProperties, cancellationToken: tokenSource.Token); - _destClient.DidNotReceiveWithAnyArgs().GetBlobClient(Arg.Any()); - await _destBlob - .DidNotReceiveWithAnyArgs() - .UploadAsync(Arg.Any(), Arg.Any(), Arg.Any()); - - // Check errors - await _errorBlob - .DidNotReceiveWithAnyArgs() - .AppendBlockAsync(Arg.Any(), null, Arg.Any()); - Assert.Equal(expectedMessage, thrownException.Message); - } - - - [Fact] - public async Task GivenCopyFailure_WhenCopying_ThenWriteToErrorLog() - { - var instance = new InstanceMetadata( - new VersionedInstanceIdentifier("1.2", "3.4.5", "6.7.8.9.10", 1), - new InstanceProperties()); - using var fileStream = new MemoryStream(); - using var tokenSource = new CancellationTokenSource(); - - _fileStore.GetStreamingFileAsync(instance.VersionedInstanceIdentifier.Version, instance.VersionedInstanceIdentifier.Partition, fileProperties: null, cancellationToken: tokenSource.Token).Returns(fileStream); - _destClient.GetBlobClient($"{_output.OperationId:N}/results/1.2/3.4.5/6.7.8.9.10.dcm").Returns(_destBlob); - _destBlob - .UploadAsync(fileStream, Arg.Is(x => x.TransferOptions == _blobOptions.Upload), tokenSource.Token) - .Returns(Task.FromException>(new IOException("Unable to copy."))); - - Assert.False(await _sink.CopyAsync(ReadResult.ForInstance(instance), tokenSource.Token)); - - await _fileStore.Received(1).GetStreamingFileAsync(instance.VersionedInstanceIdentifier.Version, instance.VersionedInstanceIdentifier.Partition, fileProperties: null, cancellationToken: tokenSource.Token); - _destClient.Received(1).GetBlobClient($"{_output.OperationId:N}/results/1.2/3.4.5/6.7.8.9.10.dcm"); - await _destBlob - .Received(1) - .UploadAsync(fileStream, Arg.Is(x => x.TransferOptions == _blobOptions.Upload), tokenSource.Token); - - // Extension method appears to prevent the match - // _destClient.Received(1).GetAppendBlobClient($"{_output.OperationId:N}/Errors.json"); - - // Check errors - ExportErrorLogEntry error = await GetErrorsAsync(tokenSource.Token).SingleAsync(); - Assert.Equal(DicomIdentifier.ForInstance("1.2", "3.4.5", "6.7.8.9.10"), error.Identifier); - Assert.Equal("Unable to copy.", error.Error); - } - - [Fact] - public async Task GivenMissingContainer_WhenInitializing_ThenThrow() - { - using var tokenSource = new CancellationTokenSource(); - - Response response = Substitute.For>(); - response.Value.Returns(false); - _destClient.ExistsAsync(tokenSource.Token).Returns(Task.FromResult(response)); - - await Assert.ThrowsAsync(() => _sink.InitializeAsync(tokenSource.Token)); - - await _destClient.Received(1).ExistsAsync(tokenSource.Token); - await _errorBlob - .DidNotReceiveWithAnyArgs() - .AppendBlockAsync(default, default, default); - } - - [Theory] - [InlineData(nameof(AggregateException))] - [InlineData(nameof(AuthenticationFailedException))] - [InlineData(nameof(RequestFailedException))] - public async Task GivenThrownException_WhenInitializing_ThenWrapAndThrow(string exception) - { - using var tokenSource = new CancellationTokenSource(); - - Response response = Substitute.For>(); - response.Value.Returns(false); - - // Note: It's more likely the permissions response would be thrown when trying to append to the blob - _destClient.ExistsAsync(tokenSource.Token).Returns( - Task.FromException>(exception switch - { - nameof(AggregateException) => new AggregateException(new RequestFailedException("Connection Failed")), - nameof(AuthenticationFailedException) => new AuthenticationFailedException("Invalid tenant."), - _ => new RequestFailedException("Insufficient Permissions"), - })); - - await Assert.ThrowsAsync(() => _sink.InitializeAsync(tokenSource.Token)); - - await _destClient.Received(1).ExistsAsync(tokenSource.Token); - await _errorBlob - .DidNotReceiveWithAnyArgs() - .AppendBlockAsync(default, default, default); - } - - [Fact] - public async Task GivenValidContainer_WhenInitializing_ThenCreateErrorLog() - { - using var tokenSource = new CancellationTokenSource(); - - var errorHref = new Uri($"https://unit-test.blob.core.windows.net/mycontainer/{_output.OperationId:N}/errors.log"); - Response response = Substitute.For>(); - response.Value.Returns(true); - _destClient.ExistsAsync(tokenSource.Token).Returns(Task.FromResult(response)); - _errorBlob - .CreateIfNotExistsAsync(default, default, tokenSource.Token) - .Returns(Substitute.For>()); - _errorBlob - .Uri - .Returns(errorHref); - - Assert.Equal(errorHref, await _sink.InitializeAsync(tokenSource.Token)); - - await _destClient.Received(1).ExistsAsync(tokenSource.Token); - await _errorBlob.Received(1).CreateIfNotExistsAsync(default, default, tokenSource.Token); - - // Extension method appears to prevent the match - // _destClient.Received(1).GetAppendBlobClient($"{_output.OperationId:N}/Errors.json"); - } - - [Fact] - public async Task GivenErrors_WhenFlushing_ThenAppendBlock() - { - using var tokenSource = new CancellationTokenSource(); - - var failure1 = new ReadFailureEventArgs(DicomIdentifier.ForStudy("1.2.3"), new StudyNotFoundException("Cannot find study.")); - var failure2 = new ReadFailureEventArgs(DicomIdentifier.ForSeries("45.6", "789.10"), new SeriesNotFoundException("Cannot find series.")); - var failure3 = new ReadFailureEventArgs(DicomIdentifier.ForInstance("1.1.12", "1.3.14151", "617"), new InstanceNotFoundException("Cannot find instance.")); - - Assert.False(await _sink.CopyAsync(ReadResult.ForFailure(failure1), tokenSource.Token)); - Assert.False(await _sink.CopyAsync(ReadResult.ForFailure(failure2), tokenSource.Token)); - Assert.False(await _sink.CopyAsync(ReadResult.ForFailure(failure3), tokenSource.Token)); - - // Nothing read yet - Assert.Equal(0, _errorStream.Position); - - List errors = await GetErrorsAsync(tokenSource.Token).ToListAsync(tokenSource.Token); - Assert.Equal(3, errors.Count); - Assert.Equal(failure1.Exception.Message, errors[0].Error); - Assert.Equal(failure2.Exception.Message, errors[1].Error); - Assert.Equal(failure3.Exception.Message, errors[2].Error); - } - - public async ValueTask DisposeAsync() - { - await _sink.DisposeAsync(); - _errorStream.Dispose(); - } - - private async IAsyncEnumerable GetErrorsAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) - { - await _sink.FlushAsync(cancellationToken); - - await _errorBlob - .Received(1) - .AppendBlockAsync(Arg.Any(), null, cancellationToken); - - _errorStream.Seek(0, SeekOrigin.Begin); - using var reader = new StreamReader(_errorStream, Encoding.UTF8); - - string line; - while ((line = reader.ReadLine()) != null) - { - yield return JsonSerializer.Deserialize(line, _jsonOptions); - } - } -} - diff --git a/src/Microsoft.Health.Dicom.Blob.UnitTests/Features/Export/ExportFilePatternTests.cs b/src/Microsoft.Health.Dicom.Blob.UnitTests/Features/Export/ExportFilePatternTests.cs deleted file mode 100644 index 5ae4e7b8f8..0000000000 --- a/src/Microsoft.Health.Dicom.Blob.UnitTests/Features/Export/ExportFilePatternTests.cs +++ /dev/null @@ -1,51 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Globalization; -using Microsoft.Health.Dicom.Blob.Features.Export; -using Microsoft.Health.Dicom.Core.Features.Model; -using Xunit; - -namespace Microsoft.Health.Dicom.Blob.UnitTests.Features.Export; - -public class ExportFilePatternTests -{ - [Fact] - public void GivenInvalidPatterns_WhenParsing_ThenThrow() - { - // Placeholders is internal, so we test the scenarios inline - Assert.Throws(() => ExportFilePattern.Parse("foo\\", ExportPatternPlaceholders.Study)); - Assert.Throws(() => ExportFilePattern.Parse("bar\\baz", ExportPatternPlaceholders.Study)); - Assert.Throws(() => ExportFilePattern.Parse("%Operation%", ExportPatternPlaceholders.Study)); - Assert.Throws(() => ExportFilePattern.Parse("%HelloWorld%", ExportPatternPlaceholders.All)); - } - - [Fact] - public void GivenValidPatterns_WhenParsing_ThenCreateFormatString() - { - // Placeholders is internal, so we test the scenarios inline - Assert.Equal("foo/bar%baz", ExportFilePattern.Parse("foo/bar\\%baz", ExportPatternPlaceholders.None)); - Assert.Equal("{0:N}/{1}/other/{2}/{3}", ExportFilePattern.Parse("%OperATion%/%STudy%/other/%SerIES%/%SOPInstance%", ExportPatternPlaceholders.All)); - } - - [Fact] - public void GivenOperationPattern_WhenFormatting_ThenReplacePattern() - { - var operationId = Guid.NewGuid(); - string format = ExportFilePattern.Parse("errors/%OperATion%/folder", ExportPatternPlaceholders.All); - Assert.Equal(string.Format(CultureInfo.InvariantCulture, format, operationId), ExportFilePattern.Format(format, operationId)); - } - - [Fact] - public void GivenCompletePattern_WhenFormatting_ThenReplacePattern() - { - var operationId = Guid.NewGuid(); - string format = ExportFilePattern.Parse("%OperATion%/%STudy%/other/%SerIES%/%SOPInstance%", ExportPatternPlaceholders.All); - Assert.Equal( - string.Format(CultureInfo.InvariantCulture, format, operationId, "1", "2", "3"), - ExportFilePattern.Format(format, operationId, new VersionedInstanceIdentifier("1", "2", "3", 1))); - } -} diff --git a/src/Microsoft.Health.Dicom.Blob.UnitTests/Features/ExternalStore/ExternalStoreHealthExpiryHttpPolicyTests.cs b/src/Microsoft.Health.Dicom.Blob.UnitTests/Features/ExternalStore/ExternalStoreHealthExpiryHttpPolicyTests.cs deleted file mode 100644 index c8812d09e8..0000000000 --- a/src/Microsoft.Health.Dicom.Blob.UnitTests/Features/ExternalStore/ExternalStoreHealthExpiryHttpPolicyTests.cs +++ /dev/null @@ -1,115 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Linq; -using Azure.Core; -using Azure.Core.Pipeline; -using Microsoft.Health.Dicom.Blob.Features.ExternalStore; -using Microsoft.Health.Dicom.Blob.Utilities; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Blob.UnitTests.Features.ExternalStore; - -public class ExternalStoreHealthExpiryHttpPolicyTests -{ - private readonly ExternalBlobDataStoreConfiguration _blobDataStoreConfiguration; - private readonly ExternalStoreHealthExpiryHttpPipelinePolicy _externalStoreHealthExpiryPolicy; - - private readonly MockRequest _request; - private readonly HttpPipelinePolicy _mockPipeline = Substitute.For(); - - public ExternalStoreHealthExpiryHttpPolicyTests() - { - _blobDataStoreConfiguration = new ExternalBlobDataStoreConfiguration() - { - BlobContainerUri = new Uri("https://myBlobStore.blob.core.net/myContainer"), - StorageDirectory = "DICOM", - HealthCheckFilePath = "healthCheck/health", - HealthCheckFileExpiry = TimeSpan.FromMinutes(1), - }; - - _request = new MockRequest(); - _externalStoreHealthExpiryPolicy = new ExternalStoreHealthExpiryHttpPipelinePolicy(_blobDataStoreConfiguration); - } - - [Theory] - [InlineData("PUT")] - [InlineData("POST")] - [InlineData("PATCH")] - public void GivenHealthCheckBlob_Proccess_AddsExpiryHeaders(string requestMethod) - { - RequestUriBuilder requestUriBuilder = new RequestUriBuilder(); - requestUriBuilder.Reset(new Uri($"https://myBlobStore.blob.core.net/myContainer/DICOM/healthCheck/health{Guid.NewGuid()}.txt")); - - _request.Uri = requestUriBuilder; - _request.Method = RequestMethod.Parse(requestMethod); - HttpMessage httpMessage = new HttpMessage(_request, new ResponseClassifier()); - - _externalStoreHealthExpiryPolicy.Process(httpMessage, new ReadOnlyMemory(new HttpPipelinePolicy[] { _mockPipeline })); - - _request.MockHeaders.Single(h => h.Name == "x-ms-expiry-time" && h.Value == $"{_blobDataStoreConfiguration.HealthCheckFileExpiry.TotalMilliseconds}"); - _request.MockHeaders.Single(h => h.Name == "x-ms-expiry-option" && h.Value == "RelativeToNow"); - } - - [Theory] - [InlineData("PUT")] - [InlineData("POST")] - [InlineData("PATCH")] - [InlineData("GET")] - [InlineData("DELETE")] - public void GivenNonHealthCheckBlob_ProccessForAllRequestTypes_NoHeadersAdded(string requestMethod) - { - RequestUriBuilder requestUriBuilder = new RequestUriBuilder(); - requestUriBuilder.Reset(new Uri($"https://myBlobStore.blob.core.net/myContainer/DICOM/healthCheck/health{Guid.NewGuid()}.txt/anotherBlob")); - - _request.Uri = requestUriBuilder; - _request.Method = RequestMethod.Parse(requestMethod); - HttpMessage httpMessage = new HttpMessage(_request, new ResponseClassifier()); - - _externalStoreHealthExpiryPolicy.Process(httpMessage, new ReadOnlyMemory(new HttpPipelinePolicy[] { _mockPipeline })); - - Assert.Empty(_request.MockHeaders); - } - - [Theory] - [InlineData("https://blob.com")] - [InlineData("https://myBlobStore.blob.core.net/myContainer/DICOM/anotherDirectory/healthCheck/health00000000-0000-0000-0000-000000000000/anotherBlob")] - [InlineData("https://myBlobStore.blob.core.net/myContainer/DICOM/anotherDirectory/healthCheck/health00000000-0000-0000-0000-000000000000.txt")] - [InlineData("https://myBlobStore.blob.core.net/myContainer/DICOM/healthCheck/health00000000-0000-0000-0000-000000000000")] - [InlineData("https://myBlobStore.blob.core.net/myContainer/DICOM/healthCheck/health")] - [InlineData("https://myBlobStore.blob.core.net/myContainer/DICOM/healthCheck")] - public void GivenNonHealthCheckBlob_ProccessDifferentBlobUris_NoHeadersAdded(string nonHealthCheckBlob) - { - RequestUriBuilder requestUriBuilder = new RequestUriBuilder(); - requestUriBuilder.Reset(new Uri(nonHealthCheckBlob)); - - _request.Uri = requestUriBuilder; - _request.Method = RequestMethod.Put; - HttpMessage httpMessage = new HttpMessage(_request, new ResponseClassifier()); - - _externalStoreHealthExpiryPolicy.Process(httpMessage, new ReadOnlyMemory(new HttpPipelinePolicy[] { _mockPipeline })); - - Assert.Empty(_request.MockHeaders); - } - - [Theory] - [InlineData("GET")] - [InlineData("DELETE")] - public void GivenHealthCheckBlobReadOrDelete_Proccess_AddsExpiryHeaders(string requestMethod) - { - RequestUriBuilder requestUriBuilder = new RequestUriBuilder(); - requestUriBuilder.Reset(new Uri($"https://myBlobStore.blob.core.net/myContainer/DICOM/healthCheck/health{Guid.NewGuid()}.txt")); - - _request.Uri = requestUriBuilder; - _request.Method = RequestMethod.Parse(requestMethod); - HttpMessage httpMessage = new HttpMessage(_request, new ResponseClassifier()); - - _externalStoreHealthExpiryPolicy.Process(httpMessage, new ReadOnlyMemory(new HttpPipelinePolicy[] { _mockPipeline })); - - Assert.Empty(_request.MockHeaders); - } -} diff --git a/src/Microsoft.Health.Dicom.Blob.UnitTests/Features/ExternalStore/MockRequest.cs b/src/Microsoft.Health.Dicom.Blob.UnitTests/Features/ExternalStore/MockRequest.cs deleted file mode 100644 index 938f0b1347..0000000000 --- a/src/Microsoft.Health.Dicom.Blob.UnitTests/Features/ExternalStore/MockRequest.cs +++ /dev/null @@ -1,45 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using Azure.Core; - -namespace Microsoft.Health.Dicom.Blob.UnitTests.Features.ExternalStore; - -internal class MockRequest : Request -{ - public MockRequest() - { - MockHeaders = new List(); - } - - public List MockHeaders { get; } - - public override string ClientRequestId { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } - - public override void Dispose() => throw new NotImplementedException(); - - protected override void AddHeader(string name, string value) - { - MockHeaders.Add(new HttpHeader(name, value)); - } - - protected override bool ContainsHeader(string name) - { - return MockHeaders.Any(h => h.Name == name); - } - - protected override IEnumerable EnumerateHeaders() - { - return MockHeaders; - } - - protected override bool RemoveHeader(string name) => throw new NotImplementedException(); - protected override bool TryGetHeader(string name, [NotNullWhen(true)] out string value) => throw new NotImplementedException(); - protected override bool TryGetHeaderValues(string name, [NotNullWhen(true)] out IEnumerable values) => throw new NotImplementedException(); -} diff --git a/src/Microsoft.Health.Dicom.Blob.UnitTests/Features/Health/DicomConnectedStoreHealthCheckTests.cs b/src/Microsoft.Health.Dicom.Blob.UnitTests/Features/Health/DicomConnectedStoreHealthCheckTests.cs deleted file mode 100644 index 6b8d52041e..0000000000 --- a/src/Microsoft.Health.Dicom.Blob.UnitTests/Features/Health/DicomConnectedStoreHealthCheckTests.cs +++ /dev/null @@ -1,153 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using Azure; -using Azure.Storage.Blobs; -using Azure.Storage.Blobs.Models; -using Azure.Storage.Blobs.Specialized; -using Microsoft.Extensions.Diagnostics.HealthChecks; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using Microsoft.Health.Core.Features.Health; -using Microsoft.Health.Dicom.Blob.Features.Health; -using Microsoft.Health.Dicom.Blob.Features.Storage; -using Microsoft.Health.Dicom.Blob.Utilities; -using NSubstitute; -using NSubstitute.ExceptionExtensions; -using Xunit; - -namespace Microsoft.Health.Dicom.Blob.UnitTests.Features.Health; - -public class DicomConnectedStoreHealthCheckTests -{ - private readonly BlobContainerClient _blobContainerClient = Substitute.For(); - private readonly BlockBlobClient _blockBlobClient = Substitute.For(); - private readonly IBlobClient _blobClient = Substitute.For(); - private readonly IOptions _externalBlobOptions = Substitute.For>(); - - private readonly DicomConnectedStoreHealthCheck _dicomConnectedStoreHealthCheck; - - public DicomConnectedStoreHealthCheckTests() - { - _blobClient.BlobContainerClient.Returns(_blobContainerClient); - _blobContainerClient.GetBlockBlobClient(Arg.Any()).Returns(_blockBlobClient); - - _externalBlobOptions.Value.Returns(new ExternalBlobDataStoreConfiguration() - { - StorageDirectory = "AHDS/", - }); - - var logger = new NullLogger(); - - _dicomConnectedStoreHealthCheck = new DicomConnectedStoreHealthCheck(_blobClient, _externalBlobOptions, logger); - } - - [Fact] - public async Task GivenHealthyConnection_RunHealthCheck_ReturnsHealthy() - { - HealthCheckResult result = await _dicomConnectedStoreHealthCheck.CheckHealthAsync(null, CancellationToken.None); - - Assert.Equal(HealthStatus.Healthy, result.Status); - } - - [Theory] - [InlineData(400, "UnsupportedHeader")] - [InlineData(401, "InvalidAuthenticationInfo")] - [InlineData(403, "InvalidAuthenticationInfo")] - [InlineData(403, "AuthorizationFailure")] - [InlineData(403, "AuthorizationPermissionMismatch")] - [InlineData(403, "InsufficientAccountPermissions")] - [InlineData(403, "AccountIsDisabled")] - [InlineData(403, "KeyVaultEncryptionKeyNotFound")] - [InlineData(403, "KeyVaultAccessTokenCannotBeAcquired")] - [InlineData(403, "KeyVaultVaultNotFound")] - [InlineData(404, "ContainerNotFound")] - [InlineData(404, "FilesystemNotFound")] - [InlineData(409, "ContainerBeingDeleted")] - [InlineData(409, "ContainerDisabled")] - public async Task GivenRequestFailedExceptionForCustomerError_RunHealthCheck_ReturnsDegraded(int statusCode, string blobErrorCode) - { - _blockBlobClient.DownloadContentAsync(Arg.Any()) - .Throws(new RequestFailedException(statusCode, "Error", blobErrorCode, new System.Exception())); - - HealthCheckResult result = await _dicomConnectedStoreHealthCheck.CheckHealthAsync(null, CancellationToken.None); - - result.Data.TryGetValue("Reason", out object healthStatusReason); - - Assert.Equal(HealthStatus.Degraded, result.Status); - Assert.Equal(HealthStatusReason.ConnectedStoreDegraded.ToString(), healthStatusReason.ToString()); - } - - [Theory] - [InlineData(400, "SomeErrorFromDicomBug")] - [InlineData(401, "AuthErrorFromDicomBug")] - [InlineData(403, "AuthErrorFromDicomBug")] - [InlineData(404, "NotFoundDueToDicomBug")] - [InlineData(409, "ConflictDueToDicomBug")] - public async Task GivenRequestFailedExceptionForServiceError_RunHealthCheck_ThrowsException(int statusCode, string blobErrorCode) - { - _blockBlobClient.DownloadContentAsync(Arg.Any()) - .Throws(new RequestFailedException(statusCode, "Error", blobErrorCode, new System.Exception())); - - await Assert.ThrowsAsync(async () => await _dicomConnectedStoreHealthCheck.CheckHealthAsync(null, CancellationToken.None)); - } - - [Fact] - public async Task HostNotFound_RunHealthCheck_ReturnsDegraded() - { - _blockBlobClient.UploadAsync(Arg.Any(), cancellationToken: Arg.Any()) - .Throws(new AggregateException(new List() - { - new Exception("No such host is known."), - new Exception("No such host is known."), - new Exception("No such host is known."), - })); - - HealthCheckResult result = await _dicomConnectedStoreHealthCheck.CheckHealthAsync(null, CancellationToken.None); - - result.Data.TryGetValue("Reason", out object healthStatusReason); - - Assert.Equal(HealthStatus.Degraded, result.Status); - Assert.Equal(HealthStatusReason.ConnectedStoreDegraded.ToString(), healthStatusReason.ToString()); - } - - [Fact] - public async Task NameOrServiceNotKnown_RunHealthCheck_ReturnsDegraded() - { - _blockBlobClient.UploadAsync(Arg.Any(), cancellationToken: Arg.Any()) - .Throws(new AggregateException(new List() - { - new Exception("Name or service not known."), - new Exception("Name or service not known."), - new Exception("Name or service not known."), - })); - - HealthCheckResult result = await _dicomConnectedStoreHealthCheck.CheckHealthAsync(null, CancellationToken.None); - - result.Data.TryGetValue("Reason", out object healthStatusReason); - - Assert.Equal(HealthStatus.Degraded, result.Status); - Assert.Equal(HealthStatusReason.ConnectedStoreDegraded.ToString(), healthStatusReason.ToString()); - } - - [Fact] - public async Task DeleteFails_RunHealthCheck_ReturnsDegradedAndExceptionIsNotThrown() - { - _blockBlobClient.DeleteAsync(Arg.Any(), Arg.Any(), Arg.Any()) - .Throws(new RequestFailedException(403, "Failure", BlobErrorCode.AuthorizationFailure.ToString(), new System.Exception())); - - HealthCheckResult result = await _dicomConnectedStoreHealthCheck.CheckHealthAsync(null, CancellationToken.None); - - result.Data.TryGetValue("Reason", out object healthStatusReason); - - Assert.Equal(HealthStatus.Degraded, result.Status); - Assert.Equal(HealthStatusReason.ConnectedStoreDegraded.ToString(), healthStatusReason.ToString()); - } -} diff --git a/src/Microsoft.Health.Dicom.Blob.UnitTests/Features/Storage/BlobFileStoreTests.cs b/src/Microsoft.Health.Dicom.Blob.UnitTests/Features/Storage/BlobFileStoreTests.cs deleted file mode 100644 index 9b76acf15e..0000000000 --- a/src/Microsoft.Health.Dicom.Blob.UnitTests/Features/Storage/BlobFileStoreTests.cs +++ /dev/null @@ -1,551 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Azure; -using Azure.Storage.Blobs; -using Azure.Storage.Blobs.Models; -using Azure.Storage.Blobs.Specialized; -using Microsoft.ApplicationInsights; -using Microsoft.ApplicationInsights.Extensibility; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using Microsoft.Health.Blob.Configs; -using Microsoft.Health.Core.Features.Identity; -using Microsoft.Health.Dicom.Blob.Features.ExternalStore; -using Microsoft.Health.Dicom.Blob.Features.Storage; -using Microsoft.Health.Dicom.Blob.Features.Telemetry; -using Microsoft.Health.Dicom.Blob.Utilities; -using Microsoft.Health.Dicom.Core; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using NSubstitute; -using NSubstitute.ExceptionExtensions; -using Xunit; - -namespace Microsoft.Health.Dicom.Blob.UnitTests.Features.Storage; - -public class BlobFileStoreTests -{ - private const string DefaultBlobName = "foo/123.dcm"; - private const string DefaultStorageDirectory = "/test/"; - private const string HealthCheckFilePath = "health"; - private static readonly BlobFileStoreMeter BlobFileStoreMeter = new BlobFileStoreMeter(); - private static readonly Uri BlobContainerUrl = new Uri("https://myBlobAccount.blob.core.net/myContainer"); - - private readonly FileProperties _defaultFileProperties = new FileProperties - { - Path = DefaultBlobName, - ETag = "45678", - ContentLength = 123 - }; - - [Theory] - [InlineData("a!/b")] - [InlineData("a#/b")] - [InlineData("a\b")] - [InlineData("a%b")] - public void GivenInvalidStorageDirectory_WhenExternalStoreInitialized_ThenThrowExceptionWithRightMessageAndProperty(string storageDirectory) - { - ExternalBlobDataStoreConfiguration config = new ExternalBlobDataStoreConfiguration() { BlobContainerUri = BlobContainerUrl, StorageDirectory = storageDirectory, HealthCheckFilePath = HealthCheckFilePath, HealthCheckFileExpiry = TimeSpan.FromMinutes(1) }; - var results = new List(); - - Assert.False(Validator.TryValidateObject(config, new ValidationContext(config), results, validateAllProperties: true)); - - Assert.Single(results); - Assert.Equal("""The field StorageDirectory must match the regular expression '^[a-zA-Z0-9\-\.]*(\/[a-zA-Z0-9\-\.]*){0,254}$'.""", results.First().ErrorMessage); - } - - [Theory] - [InlineData("//")] - [InlineData("a/")] - [InlineData("a")] - [InlineData("a/b/")] - [InlineData("a-b/c-d/")] - public void GivenValidStorageDirectory_WhenExternalStoreInitialized_ThenDoNotThrowException(string storageDirectory) - { - ExternalBlobDataStoreConfiguration config = new ExternalBlobDataStoreConfiguration() { BlobContainerUri = BlobContainerUrl, StorageDirectory = storageDirectory, HealthCheckFilePath = HealthCheckFilePath, HealthCheckFileExpiry = TimeSpan.FromMinutes(1) }; - var results = new List(); - - Assert.True(Validator.TryValidateObject(config, new ValidationContext(config), results, validateAllProperties: true)); - } - - [Fact] - public void GivenInvalidStorageDirectorySegments_WhenExternalStoreInitialized_ThenThrowExceptionWithRightMessageAndProperty() - { - ExternalBlobDataStoreConfiguration config = new ExternalBlobDataStoreConfiguration() { BlobContainerUri = BlobContainerUrl, StorageDirectory = string.Join("", Enumerable.Repeat("a/b", 255)), HealthCheckFilePath = HealthCheckFilePath, HealthCheckFileExpiry = TimeSpan.FromMinutes(1) }; - var results = new List(); - - Assert.False(Validator.TryValidateObject(config, new ValidationContext(config), results, validateAllProperties: true)); - - Assert.Single(results); - Assert.Equal("""The field StorageDirectory must match the regular expression '^[a-zA-Z0-9\-\.]*(\/[a-zA-Z0-9\-\.]*){0,254}$'.""", results.First().ErrorMessage); - } - - [Fact] - public void GivenInvalidStorageDirectoryLength_WhenExternalStoreInitialized_ThenThrowExceptionWithRightMessageAndProperty() - { - ExternalBlobDataStoreConfiguration config = new ExternalBlobDataStoreConfiguration() { BlobContainerUri = BlobContainerUrl, StorageDirectory = string.Join("", Enumerable.Repeat("a", 1025)), HealthCheckFilePath = HealthCheckFilePath, HealthCheckFileExpiry = TimeSpan.FromMinutes(1) }; - var results = new List(); - - Assert.False(Validator.TryValidateObject(config, new ValidationContext(config), results, validateAllProperties: true)); - - Assert.Single(results); - Assert.Equal("""The field StorageDirectory must be a string with a maximum length of 1024.""", results.First().ErrorMessage); - } - - [Fact] - public async Task GivenExternalStore_WhenUploadFails_ThenThrowExceptionWithRightMessageAndProperty() - { - InitializeExternalBlobFileStore(out BlobFileStore blobFileStore, out ExternalBlobClient client); - client.BlobContainerClient.GetBlockBlobClient(DefaultBlobName).UploadAsync(Arg.Any(), Arg.Any(), Arg.Any()).Throws - (new System.Exception()); - - var ex = await Assert.ThrowsAsync(() => blobFileStore.StoreFileAsync(1, Partition.DefaultName, Substitute.For(), CancellationToken.None)); - - Assert.True(ex.IsExternal); - Assert.Equal(string.Format(CultureInfo.InvariantCulture, DicomCoreResource.ExternalDataStoreOperationFailed, new System.Exception().Message), ex.Message); - } - - [Fact] - public async Task GivenInternalStore_WhenUploadFails_ThenThrowExceptionWithRightMessageAndProperty() - { - InitializeInternalBlobFileStore(out BlobFileStore blobFileStore, out TestInternalBlobClient client); - client.BlockBlobClient.UploadAsync(Arg.Any(), Arg.Any(), Arg.Any()).Throws(new System.Exception()); - - var ex = await Assert.ThrowsAsync(() => blobFileStore.StoreFileAsync(1, Partition.DefaultName, Substitute.For(), CancellationToken.None)); - - Assert.False(ex.IsExternal); - Assert.Equal(DicomCoreResource.DataStoreOperationFailed, ex.Message); - } - - [Fact] - public async Task GivenExternalStore_WhenGetStreamingFileAsyncFailsBecauseBlobNotFound_ThenThrowExceptionWithRightMessageAndProperty() - { - InitializeExternalBlobFileStore(out BlobFileStore blobFileStore, out ExternalBlobClient client); - RequestFailedException requestFailedException = new RequestFailedException(status: 404, message: "test", errorCode: BlobErrorCode.BlobNotFound.ToString(), innerException: null); - client.BlobContainerClient.GetBlockBlobClient(DefaultBlobName).DownloadStreamingAsync( - Arg.Any(), - Arg.Any(), - false, - Arg.Any()).Throws(requestFailedException); - - var ex = await Assert.ThrowsAsync(() => blobFileStore.GetStreamingFileAsync(1, Partition.Default, _defaultFileProperties, CancellationToken.None)); - - Assert.True(ex.IsExternal); - Assert.Equal(string.Format(CultureInfo.InvariantCulture, DicomCoreResource.ExternalDataStoreOperationFailed, BlobErrorCode.BlobNotFound.ToString()), ex.Message); - } - - [Fact] - public async Task GivenExternalStore_WhenGetStreamingFileAsyncFailsBecauseConditionsNotMet_ThenThrowExceptionWithRightMessageAndProperty() - { - InitializeExternalBlobFileStore(out BlobFileStore blobFileStore, out ExternalBlobClient client); - - RequestFailedException requestFailedException = new RequestFailedException( - status: 412, - message: "Condition was not met.", - errorCode: BlobErrorCode.ConditionNotMet.ToString(), - innerException: new Exception("Condition not met.")); - - client.BlobContainerClient.GetBlockBlobClient(DefaultBlobName).DownloadStreamingAsync( - Arg.Any(), - Arg.Any(), - false, - Arg.Any()).Throws(requestFailedException); - - var ex = await Assert.ThrowsAsync(() => blobFileStore.GetStreamingFileAsync(1, Partition.Default, _defaultFileProperties, CancellationToken.None)); - - Assert.True(ex.IsExternal); - Assert.Equal(string.Format(CultureInfo.InvariantCulture, DicomCoreResource.ExternalDataStoreOperationFailed, BlobErrorCode.ConditionNotMet.ToString()), ex.Message); - } - - [Fact] - public async Task GivenExternalStore_WhenRequestFails_ThenThrowExceptionWithRightMessageAndProperty() - { - InitializeExternalBlobFileStore(out BlobFileStore blobFileStore, out ExternalBlobClient client); - RequestFailedException requestFailedAuthException = new RequestFailedException( - status: 400, - message: "auth failed simulation", - errorCode: BlobErrorCode.AuthenticationFailed.ToString(), - innerException: new Exception("super secret inner info")); - client.BlobContainerClient.GetBlockBlobClient(DefaultBlobName).DownloadStreamingAsync( - Arg.Any(), - Arg.Any(), - false, - Arg.Any()).Throws(requestFailedAuthException); - - var ex = await Assert.ThrowsAsync(() => blobFileStore.GetStreamingFileAsync(1, Partition.Default, null, CancellationToken.None)); - - Assert.True(ex.IsExternal); - Assert.Equal(string.Format(CultureInfo.InvariantCulture, DicomCoreResource.ExternalDataStoreOperationFailed, BlobErrorCode.AuthenticationFailed.ToString()), ex.Message); - } - - [Fact] - public async Task GivenExternalStore_WhenGetFileAsync_ThenExpectConditionsUsed() - { - InitializeExternalBlobFileStore(out BlobFileStore blobFileStore, out ExternalBlobClient client); - - RequestFailedException requestFailedException = new RequestFailedException( - status: 412, - message: "Condition was not met.", - errorCode: BlobErrorCode.ConditionNotMet.ToString(), - innerException: new Exception("Condition not met.")); - - client.BlobContainerClient.GetBlockBlobClient(DefaultBlobName).OpenReadAsync( - Arg.Is(options => options.Conditions.IfMatch.ToString() == _defaultFileProperties.ETag), - Arg.Any()).Throws(requestFailedException); - - var ex = await Assert.ThrowsAsync(() => blobFileStore.GetFileAsync(1, Partition.Default, _defaultFileProperties, CancellationToken.None)); - - Assert.True(ex.IsExternal); - Assert.Equal(string.Format(CultureInfo.InvariantCulture, DicomCoreResource.ExternalDataStoreOperationFailed, BlobErrorCode.ConditionNotMet.ToString()), ex.Message); - } - - [Fact] - public async Task GivenInternalStore_WhenGetFileAsync_ThenExpectNoConditionsUsed() - { - InitializeInternalBlobFileStore(out BlobFileStore blobFileStore, out TestInternalBlobClient client); - - var expectedResult = Substitute.For(); - client.BlobContainerClient.GetBlockBlobClient(DefaultBlobName).OpenReadAsync( - Arg.Any(), - Arg.Any()).Returns(expectedResult); - - var result = await blobFileStore.GetFileAsync(1, Partition.Default, _defaultFileProperties, CancellationToken.None); - Assert.Equal(expectedResult, result); - await client.BlobContainerClient.GetBlockBlobClient(DefaultBlobName).Received(1).OpenReadAsync( - Arg.Is(options => options.Conditions == null), - Arg.Any()); - } - - [Fact] - public async Task GivenExternalStore_WhenDeleteFileEtagMatchConditionMet_AndFileDoesExist_ThenExpectNoExceptionThrown() - { - InitializeExternalBlobFileStore(out BlobFileStore blobFileStore, out ExternalBlobClient client); - var expectedResult = Substitute.For>(); - expectedResult.Value.Returns(true); - - client.BlobContainerClient.GetBlockBlobClient(DefaultBlobName).DeleteIfExistsAsync( - Arg.Any(), - conditions: Arg.Any(), - Arg.Any()).Returns(expectedResult); - - await blobFileStore.DeleteFileIfExistsAsync(1, Partition.Default, _defaultFileProperties, CancellationToken.None); - - await client.BlobContainerClient.GetBlockBlobClient(DefaultBlobName).Received(1).DeleteIfExistsAsync( - Arg.Is(options => options == DeleteSnapshotsOption.IncludeSnapshots), - Arg.Is(conditions => conditions.IfMatch.ToString() == _defaultFileProperties.ETag), - Arg.Any()); - } - - [Fact] - public async Task GivenExternalStore_WhenDeleteFileConditionNotMet_EvenIfFileDoesExist_ThenExpectExceptionNotThrown() - { - InitializeExternalBlobFileStore(out BlobFileStore blobFileStore, out ExternalBlobClient client); - - RequestFailedException requestFailedException = new RequestFailedException( - status: 412, - message: "Condition was not met.", - errorCode: BlobErrorCode.ConditionNotMet.ToString(), - innerException: new Exception("Condition not met.")); - - client.BlobContainerClient.GetBlockBlobClient(DefaultBlobName).DeleteIfExistsAsync( - Arg.Any(), - conditions: Arg.Any(), - Arg.Any()).Throws(requestFailedException); - - // NOTE today the blob client will throw on ConditionNotMet when the file does not exist, so there is no discernible difference - // when a file exists but etag doesnt match and a file not existing. Either way, we will not retry when this occurs, so we will not throw an - // exception - await blobFileStore.DeleteFileIfExistsAsync(1, Partition.Default, _defaultFileProperties, CancellationToken.None); - - await client.BlobContainerClient.GetBlockBlobClient(DefaultBlobName).Received(1).DeleteIfExistsAsync( - Arg.Is(options => options == DeleteSnapshotsOption.IncludeSnapshots), - Arg.Is(conditions => conditions.IfMatch.ToString() == _defaultFileProperties.ETag), - Arg.Any()); - } - - [Fact] - public async Task GivenInternalStore_WhenDeleteAsync_ThenExpectNoConditionsUsed() - { - InitializeInternalBlobFileStore(out BlobFileStore blobFileStore, out TestInternalBlobClient client); - var expectedResult = Substitute.For>(); - expectedResult.Value.Returns(true); - - client.BlobContainerClient.GetBlockBlobClient(DefaultBlobName).DeleteIfExistsAsync( - DeleteSnapshotsOption.IncludeSnapshots, - conditions: null, - Arg.Any()).Returns(expectedResult); - - await blobFileStore.DeleteFileIfExistsAsync(1, Partition.Default, _defaultFileProperties, CancellationToken.None); - - await client.BlobContainerClient.GetBlockBlobClient(DefaultBlobName).Received(1).DeleteIfExistsAsync( - Arg.Is(options => options == DeleteSnapshotsOption.IncludeSnapshots), - null, - Arg.Any()); - } - - [Fact] - public async Task GivenInternalStore_WhenDeleteAsyncOnAFileThatDoesNotExist_ThenNoExceptionsThrown() - { - InitializeInternalBlobFileStore(out BlobFileStore blobFileStore, out TestInternalBlobClient client); - var expectedResult = Substitute.For>(); - expectedResult.Value.Returns(false); - - client.BlobContainerClient.GetBlockBlobClient(DefaultBlobName).DeleteIfExistsAsync( - DeleteSnapshotsOption.IncludeSnapshots, - conditions: null, - Arg.Any()).Returns(expectedResult); - - await blobFileStore.DeleteFileIfExistsAsync(1, Partition.Default, _defaultFileProperties, CancellationToken.None); - } - - [Fact] - public async Task GivenExternalStore_WhenCopyFileAsync_ThenExpectConditionsUsed() - { - InitializeExternalBlobFileStore(out BlobFileStore blobFileStore, out ExternalBlobClient client); - - RequestFailedException requestFailedException = new RequestFailedException( - status: 412, - message: "Condition was not met.", - errorCode: BlobErrorCode.ConditionNotMet.ToString(), - innerException: new Exception("Condition not met.")); - - client.BlobContainerClient.GetBlockBlobClient(DefaultBlobName).StartCopyFromUriAsync( - Arg.Any(), - Arg.Any(), - Arg.Any()).Throws(requestFailedException); - - var ex = await Assert.ThrowsAsync(() => blobFileStore.CopyFileAsync(1, 2, Partition.Default, _defaultFileProperties, CancellationToken.None)); - - Assert.True(ex.IsExternal); - Assert.Equal(string.Format(CultureInfo.InvariantCulture, DicomCoreResource.ExternalDataStoreOperationFailed, BlobErrorCode.ConditionNotMet.ToString()), ex.Message); - await client.BlobContainerClient.GetBlockBlobClient(DefaultBlobName).Received(1).StartCopyFromUriAsync( - Arg.Any(), - Arg.Is(options => options.SourceConditions.IfMatch.ToString() == _defaultFileProperties.ETag), - Arg.Any()); - } - - [Fact] - public async Task GivenInternalStore_WhenCopyFileAsync_ThenExpectNoConditionsUsed() - { - InitializeInternalBlobFileStore(out BlobFileStore blobFileStore, out TestInternalBlobClient client); - - var expectedResult = Substitute.For>(); - var operation = Substitute.For(); - operation.WaitForCompletionAsync(Arg.Any()).Returns(expectedResult); - - client.BlobContainerClient.GetBlockBlobClient(DefaultBlobName).StartCopyFromUriAsync( - Arg.Any(), - Arg.Any(), - Arg.Any()).Returns(operation); - - await blobFileStore.CopyFileAsync(1, 2, Partition.Default, _defaultFileProperties, CancellationToken.None); - - await client.BlobContainerClient.GetBlockBlobClient(DefaultBlobName).Received(1).StartCopyFromUriAsync( - Arg.Any(), - Arg.Is(options => options.SourceConditions == null), - Arg.Any()); - } - - [Fact] - public async Task GivenExternalStore_WhenGetFileContentInRangeAsync_ThenExpectConditionsUsed() - { - InitializeExternalBlobFileStore(out BlobFileStore blobFileStore, out ExternalBlobClient client); - - FrameRange range = new FrameRange(offset: 0, length: 100); - - RequestFailedException requestFailedException = new RequestFailedException( - status: 412, - message: "Condition was not met.", - errorCode: BlobErrorCode.ConditionNotMet.ToString(), - innerException: new Exception("Condition not met.")); - - client.BlobContainerClient.GetBlockBlobClient(DefaultBlobName).DownloadContentAsync( - Arg.Any(), - Arg.Any()).Throws(requestFailedException); - - var ex = await Assert.ThrowsAsync(() => blobFileStore.GetFileContentInRangeAsync(1, Partition.Default, _defaultFileProperties, range, CancellationToken.None)); - - Assert.True(ex.IsExternal); - Assert.Equal(string.Format(CultureInfo.InvariantCulture, DicomCoreResource.ExternalDataStoreOperationFailed, BlobErrorCode.ConditionNotMet.ToString()), ex.Message); - - await client.BlobContainerClient.GetBlockBlobClient(DefaultBlobName).Received(1).DownloadContentAsync( - Arg.Is(options => - options.Conditions.IfMatch.ToString() == _defaultFileProperties.ETag - && options.Range.Offset == range.Offset - && options.Range.Length == range.Length), - Arg.Any()); - } - - [Fact] - public async Task GivenExternalStore_WhenGetFilePropertiesAsync_ThenExpectConditionsUsed() - { - InitializeExternalBlobFileStore(out BlobFileStore blobFileStore, out ExternalBlobClient client); - - FrameRange range = new FrameRange(offset: 0, length: 100); - - RequestFailedException requestFailedException = new RequestFailedException( - status: 412, - message: "Condition was not met.", - errorCode: BlobErrorCode.ConditionNotMet.ToString(), - innerException: new Exception("Condition not met.")); - - client.BlobContainerClient.GetBlockBlobClient(DefaultBlobName).GetPropertiesAsync( - Arg.Any(), - Arg.Any()).Throws(requestFailedException); - - var ex = await Assert.ThrowsAsync(() => blobFileStore.GetFilePropertiesAsync(1, Partition.Default, _defaultFileProperties, CancellationToken.None)); - - Assert.True(ex.IsExternal); - Assert.Equal(string.Format(CultureInfo.InvariantCulture, DicomCoreResource.ExternalDataStoreOperationFailed, BlobErrorCode.ConditionNotMet.ToString()), ex.Message); - - await client.BlobContainerClient.GetBlockBlobClient(DefaultBlobName).Received(1).GetPropertiesAsync( - Arg.Is(options => - options.IfMatch.ToString() == _defaultFileProperties.ETag), - Arg.Any()); - } - - [Fact] - public async Task GivenInternalStore_WhenGetFileContentInRangeAsync_ThenExpectConditionsNotUsed() - { - InitializeInternalBlobFileStore(out BlobFileStore blobFileStore, out TestInternalBlobClient client); - - FrameRange range = new FrameRange(offset: 0, length: 100); - - var expectedResult = Substitute.For>(); - var blobDownloadResult = BlobsModelFactory.BlobDownloadResult(details: CreateBlobDownloadDetails(range)); - - expectedResult.Value.Returns(blobDownloadResult); - - client.BlobContainerClient.GetBlockBlobClient(DefaultBlobName).DownloadContentAsync( - Arg.Any(), - Arg.Any()).Returns(expectedResult); - - var result = await blobFileStore.GetFileContentInRangeAsync(1, Partition.Default, _defaultFileProperties, range, CancellationToken.None); - Assert.Equal(expectedResult.Value.Content, result); - await client.BlobContainerClient.GetBlockBlobClient(DefaultBlobName).Received(1).DownloadContentAsync( - Arg.Is(options => - options.Conditions == null), - Arg.Any()); - } - - private static BlobDownloadDetails CreateBlobDownloadDetails(FrameRange range) - { - return BlobsModelFactory.BlobDownloadDetails( - BlobType.Block, range.Length, null, null, DateTimeOffset.Now, null, - null, null, null, null, null, - -123L, DateTimeOffset.Now, null, null, null, - null, CopyStatus.Success, LeaseDurationType.Fixed, LeaseState.Available, LeaseStatus.Unlocked, null, - 200, false, null, null, null, - 5, null, false, null, null, - false, DateTimeOffset.Now); - } - - [Fact] - public void GivenExternalStore_WhenGetServiceStorePathWithPartitionsEnabled_ThenPathReturnedContainsPartitionPassed() - { - string partitionName = "foo"; - InitializeExternalBlobFileStore(out BlobFileStore _, out ExternalBlobClient client, partitionsEnabled: true); - Assert.Equal(DefaultStorageDirectory + partitionName + "/", client.GetServiceStorePath(partitionName)); - } - - [Fact] - public void GivenExternalStore_WhenGetServiceStorePathWithPartitionsDisabled_ThenPathReturnedDoesNotUsePartition() - { - InitializeExternalBlobFileStore(out BlobFileStore _, out ExternalBlobClient client, partitionsEnabled: false); - Assert.Equal(DefaultStorageDirectory, client.GetServiceStorePath("foo")); - } - - [Fact] - public async Task GivenInternalStore_WhenGetPropertiesFails_ThenThrowExceptionWithRightMessageAndProperty() - { - InitializeInternalBlobFileStore(out BlobFileStore blobFileStore, out TestInternalBlobClient client); - client.BlockBlobClient.GetPropertiesAsync(Arg.Any(), Arg.Any()).Throws(new System.Exception()); - - var ex = await Assert.ThrowsAsync(() => blobFileStore.GetFilePropertiesAsync(1, Partition.Default, null, CancellationToken.None)); - - Assert.False(ex.IsExternal); - Assert.Equal(DicomCoreResource.DataStoreOperationFailed, ex.Message); - } - - private static void InitializeInternalBlobFileStore(out BlobFileStore blobFileStore, out TestInternalBlobClient externalBlobClient) - { - externalBlobClient = new TestInternalBlobClient(); - var options = Substitute.For>(); - options.Value.Returns(Substitute.For()); - blobFileStore = new BlobFileStore(externalBlobClient, Substitute.For(), options, BlobFileStoreMeter, NullLogger.Instance, new TelemetryClient(new TelemetryConfiguration())); - - } - - private static void InitializeExternalBlobFileStore(out BlobFileStore blobFileStore, out ExternalBlobClient externalBlobClient, bool partitionsEnabled = false) - { - var featureConfiguration = Substitute.For>(); - featureConfiguration.Value.Returns(new FeatureConfiguration - { - EnableDataPartitions = partitionsEnabled, - }); - var externalStoreConfig = Substitute.For>(); - externalStoreConfig.Value.Returns(new ExternalBlobDataStoreConfiguration - { - ConnectionString = "test", - ContainerName = "test", - StorageDirectory = DefaultStorageDirectory, - BlobContainerUri = BlobContainerUrl, - HealthCheckFilePath = HealthCheckFilePath, - HealthCheckFileExpiry = TimeSpan.FromMinutes(1), - }); - var clientOptions = Substitute.For>(); - clientOptions.Value.Returns(Substitute.For()); - externalBlobClient = new ExternalBlobClient( - Substitute.For(), - externalStoreConfig, - clientOptions, - featureConfiguration, - NullLogger.Instance); - - var blobContainerClient = Substitute.For(); - blobContainerClient.GetBlockBlobClient(Arg.Any()).Returns(Substitute.For()); - externalBlobClient.BlobContainerClient = blobContainerClient; - - var options = Substitute.For>(); - options.Value.Returns(Substitute.For()); - blobFileStore = new BlobFileStore(externalBlobClient, Substitute.For(), options, BlobFileStoreMeter, NullLogger.Instance, new TelemetryClient(new TelemetryConfiguration())); - } - - private class TestInternalBlobClient : IBlobClient - { - public TestInternalBlobClient() - { - BlobContainerClient = Substitute.For(); - BlockBlobClient = Substitute.For(); - BlobContainerClient.GetBlockBlobClient(Arg.Any()).Returns(BlockBlobClient); - } - - public virtual BlobContainerClient BlobContainerClient { get; private set; } - - public bool IsExternal => false; - - public BlockBlobClient BlockBlobClient { get; private set; } - - public string GetServiceStorePath(string _) - { - return "internal/"; - } - - public BlobRequestConditions GetConditions(FileProperties fileProperties) => null; - } - -} diff --git a/src/Microsoft.Health.Dicom.Blob.UnitTests/Microsoft.Health.Dicom.Blob.UnitTests.csproj b/src/Microsoft.Health.Dicom.Blob.UnitTests/Microsoft.Health.Dicom.Blob.UnitTests.csproj deleted file mode 100644 index f04e13ffe3..0000000000 --- a/src/Microsoft.Health.Dicom.Blob.UnitTests/Microsoft.Health.Dicom.Blob.UnitTests.csproj +++ /dev/null @@ -1,33 +0,0 @@ - - - - $(LibraryFrameworks) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Microsoft.Health.Dicom.Blob/AzureBlobAPIs.md b/src/Microsoft.Health.Dicom.Blob/AzureBlobAPIs.md deleted file mode 100644 index 3f9b42a2f7..0000000000 --- a/src/Microsoft.Health.Dicom.Blob/AzureBlobAPIs.md +++ /dev/null @@ -1,11 +0,0 @@ -# Azure Blob APIs - -This document provides some context on different Azure blob SDK apis. -https://github.com/Azure/azure-sdk-for-net/issues/22022 - -The `DownloadStreamingAsync` is replacement for `DownloadAsync`. -Except slightly different return type please think about this as a rename or new alias for the same functionality. -We've introduced `DownloadContentAsync` for scenarios where small sized blobs are used for formats supported by BinaryData type (e.g. json files) thus we wanted to rename existing API to make the download family less ambiguous. - -The difference between `DownloadStreamingAsync` and `OpenReadAsync` is that the former gives you a network stream (wrapped with few layers but effectively think about it as network stream) which holds on to single connection, the later on the other hand fetches payload in chunks and buffers issuing multiple requests to fetch content. -Picking one over the other one depends on the scenario, i.e. if the consuming code is fast and you have good broad network link to storage account then former might be better choice as you avoid multiple req-res exchanges but if the consumer is slow then later might be a good idea as it releases a connection back to the pool right after reading and buffering next chunk. We recommend to perf test your app with both to reveal which is best choice if it's not obvious. diff --git a/src/Microsoft.Health.Dicom.Blob/AzureBlobClientOptions.cs b/src/Microsoft.Health.Dicom.Blob/AzureBlobClientOptions.cs deleted file mode 100644 index 0911ba6e9d..0000000000 --- a/src/Microsoft.Health.Dicom.Blob/AzureBlobClientOptions.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Azure.Storage.Blobs; - -namespace Microsoft.Health.Dicom.Blob; - -/// -public sealed class AzureBlobClientOptions : BlobClientOptions -{ - // This class is a workaround for using IOptions as the BlobClientOptions ctor cannot be used as-is -} diff --git a/src/Microsoft.Health.Dicom.Blob/AzureStorageConnection.cs b/src/Microsoft.Health.Dicom.Blob/AzureStorageConnection.cs deleted file mode 100644 index 809d298a15..0000000000 --- a/src/Microsoft.Health.Dicom.Blob/AzureStorageConnection.cs +++ /dev/null @@ -1,20 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Blob; - -internal static class AzureStorageConnection -{ - public const string AccountName = "AccountName"; - - public const string AccountKey = "AccountKey"; - - public const string SharedAccessSignature = "SharedAccessSignature"; - - internal static class Uri - { - public const string Sig = "sig"; - } -} diff --git a/src/Microsoft.Health.Dicom.Blob/BlobConstants.cs b/src/Microsoft.Health.Dicom.Blob/BlobConstants.cs deleted file mode 100644 index a9fcb5e467..0000000000 --- a/src/Microsoft.Health.Dicom.Blob/BlobConstants.cs +++ /dev/null @@ -1,18 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Blob; - -public static class BlobConstants -{ - public const string BlobStoreConfigurationSection = "DicomWeb:DicomStore"; - public const string BlobContainerConfigurationName = "dicomBlob"; - - public const string MetadataStoreConfigurationSection = "DicomWeb:MetadataStore"; - public const string MetadataContainerConfigurationName = "dicomMetadata"; - - public const string WorkitemStoreConfigurationSection = "DicomWeb:WorkitemStore"; - public const string WorkitemContainerConfigurationName = "dicomWorkitem"; -} diff --git a/src/Microsoft.Health.Dicom.Blob/DicomBlobContainerOptions.cs b/src/Microsoft.Health.Dicom.Blob/DicomBlobContainerOptions.cs deleted file mode 100644 index b3ba7ea04c..0000000000 --- a/src/Microsoft.Health.Dicom.Blob/DicomBlobContainerOptions.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.ComponentModel.DataAnnotations; - -namespace Microsoft.Health.Dicom.Blob; - -/// -/// Represents the various Azure Blob Containers used by the DICOM server. -/// -public sealed class DicomBlobContainerOptions -{ - public const string SectionName = "Containers"; - - /// - /// Gets or sets the container name for metadata. - /// - /// The metadata container name. - [Required] - public string Metadata { get; set; } - - /// - /// Gets or sets the container name for files. - /// - /// The file container name. - [Required] - public string File { get; set; } -} diff --git a/src/Microsoft.Health.Dicom.Blob/DicomBlobResource.Designer.cs b/src/Microsoft.Health.Dicom.Blob/DicomBlobResource.Designer.cs deleted file mode 100644 index ae3f132bad..0000000000 --- a/src/Microsoft.Health.Dicom.Blob/DicomBlobResource.Designer.cs +++ /dev/null @@ -1,234 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Microsoft.Health.Dicom.Blob { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class DicomBlobResource { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal DicomBlobResource() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.Health.Dicom.Blob.DicomBlobResource", typeof(DicomBlobResource).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to Account key authentication is not supported.. - /// - internal static string AzureStorageAccountKeyUnsupported { - get { - return ResourceManager.GetString("AzureStorageAccountKeyUnsupported", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Unable to authenticate for Azure Blob Storage container '{0}' in account '{1}'.. - /// - internal static string BlobStorageAuthenticateFailure { - get { - return ResourceManager.GetString("BlobStorageAuthenticateFailure", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cannot connect to Azure Blob Storage container '{0}' in account '{1}'.. - /// - internal static string BlobStorageConnectionFailure { - get { - return ResourceManager.GetString("BlobStorageConnectionFailure", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Failed to write to Azure Blob Storage container '{0}' in account '{1}'.. - /// - internal static string BlobStorageRequestFailure { - get { - return ResourceManager.GetString("BlobStorageRequestFailure", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The blob does not contain commited blocks.. - /// - internal static string BlockListNotFound { - get { - return ResourceManager.GetString("BlockListNotFound", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The specified block not found.. - /// - internal static string BlockNotFound { - get { - return ResourceManager.GetString("BlockNotFound", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to UseManagedIdentity cannot be specified if the BlobContainerUri container query string parameters.. - /// - internal static string ConflictingBlobExportAuthentication { - get { - return ResourceManager.GetString("ConflictingBlobExportAuthentication", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to ConnectionString and BlobContainerName cannot be specified along with BlobContainerUri.. - /// - internal static string ConflictingExportBlobConnections { - get { - return ResourceManager.GetString("ConflictingExportBlobConnections", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Azure Blob Storage container '{0}' does not exist in account '{1}'.. - /// - internal static string ContainerDoesNotExist { - get { - return ResourceManager.GetString("ContainerDoesNotExist", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to '{0}' is an invalid escape sequence.. - /// - internal static string InvalidEscapeSequence { - get { - return ResourceManager.GetString("InvalidEscapeSequence", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Managed identity cannot be used with a ConnectionString.. - /// - internal static string InvalidExportBlobAuthentication { - get { - return ResourceManager.GetString("InvalidExportBlobAuthentication", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Could not parse pattern for '{0}' for property '{1}'.. - /// - internal static string InvalidPattern { - get { - return ResourceManager.GetString("InvalidPattern", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Could not find closing % for placeholder.. - /// - internal static string MalformedPlaceholder { - get { - return ResourceManager.GetString("MalformedPlaceholder", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Please specify both ConnectionString and BlobContainerName.. - /// - internal static string MissingExportBlobConnection { - get { - return ResourceManager.GetString("MissingExportBlobConnection", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Export settings contain sensitive data but no secret store has been configured.. - /// - internal static string MissingSecretStore { - get { - return ResourceManager.GetString("MissingSecretStore", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to No identity has been configured.. - /// - internal static string MissingServerIdentity { - get { - return ResourceManager.GetString("MissingServerIdentity", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Public access to Azure Blob Storage containers is not supported.. - /// - internal static string PublicBlobStorageConnectionUnsupported { - get { - return ResourceManager.GetString("PublicBlobStorageConnectionUnsupported", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Shared Access Signatures (SAS) are not supported.. - /// - internal static string SasTokenAuthenticationUnsupported { - get { - return ResourceManager.GetString("SasTokenAuthenticationUnsupported", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Placeholder '{0}' is not recognized.. - /// - internal static string UnknownPlaceholder { - get { - return ResourceManager.GetString("UnknownPlaceholder", resourceCulture); - } - } - } -} diff --git a/src/Microsoft.Health.Dicom.Blob/DicomBlobResource.resx b/src/Microsoft.Health.Dicom.Blob/DicomBlobResource.resx deleted file mode 100644 index e55bd72b38..0000000000 --- a/src/Microsoft.Health.Dicom.Blob/DicomBlobResource.resx +++ /dev/null @@ -1,184 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Unable to authenticate for Azure Blob Storage container '{0}' in account '{1}'. - {0} Container name. {1} Account name. - - - Cannot connect to Azure Blob Storage container '{0}' in account '{1}'. - {0} Container name. {1} Account name. - - - Failed to write to Azure Blob Storage container '{0}' in account '{1}'. - {0} Container name. {1} Account name. - - - Azure Blob Storage container '{0}' does not exist in account '{1}'. - {0} Container name. {1} Account name. - - - '{0}' is an invalid escape sequence. - {0} Escape Sequence - - - Could not parse pattern for '{0}' for property '{1}'. - {0} Pattern. {1} Property Name - - - Could not find closing % for placeholder. - - - Export settings contain sensitive data but no secret store has been configured. - - - No identity has been configured. - - - Public access to Azure Blob Storage containers is not supported. - - - Shared Access Signatures (SAS) are not supported. - - - Placeholder '{0}' is not recognized. - {0} Placeholder - - - ConnectionString and BlobContainerName cannot be specified along with BlobContainerUri. - - - Please specify both ConnectionString and BlobContainerName. - - - Managed identity cannot be used with a ConnectionString. - - - Account key authentication is not supported. - - - UseManagedIdentity cannot be specified if the BlobContainerUri container query string parameters. - - - The specified block not found. - - - The blob does not contain commited blocks. - - \ No newline at end of file diff --git a/src/Microsoft.Health.Dicom.Blob/Extensions/AzureBlobExportOptionsExtensions.cs b/src/Microsoft.Health.Dicom.Blob/Extensions/AzureBlobExportOptionsExtensions.cs deleted file mode 100644 index 9211d2e184..0000000000 --- a/src/Microsoft.Health.Dicom.Blob/Extensions/AzureBlobExportOptionsExtensions.cs +++ /dev/null @@ -1,43 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using Azure.Core; -using Azure.Storage.Blobs; -using EnsureThat; -using Microsoft.Health.Core.Features.Identity; -using Microsoft.Health.Dicom.Core.Models.Export; - -namespace Microsoft.Health.Dicom.Blob.Extensions; - -internal static class AzureBlobExportOptionsExtensions -{ - public static BlobContainerClient GetBlobContainerClient( - this AzureBlobExportOptions exportOptions, - IExternalCredentialProvider credentialProvider, - BlobClientOptions options) - { - EnsureArg.IsNotNull(exportOptions, nameof(exportOptions)); - EnsureArg.IsNotNull(options, nameof(options)); - - if (exportOptions.BlobContainerUri != null) - { - if (exportOptions.UseManagedIdentity) - { - TokenCredential credential = credentialProvider.GetTokenCredential(); - if (credential == null) - { - throw new InvalidOperationException(DicomBlobResource.MissingServerIdentity); - } - - return new BlobContainerClient(exportOptions.BlobContainerUri, credential); - } - - return new BlobContainerClient(exportOptions.BlobContainerUri); - } - - return new BlobContainerClient(exportOptions.ConnectionString, exportOptions.BlobContainerName, options); - } -} diff --git a/src/Microsoft.Health.Dicom.Blob/Extensions/BlobStorageErrorExtensions.cs b/src/Microsoft.Health.Dicom.Blob/Extensions/BlobStorageErrorExtensions.cs deleted file mode 100644 index 133509eb20..0000000000 --- a/src/Microsoft.Health.Dicom.Blob/Extensions/BlobStorageErrorExtensions.cs +++ /dev/null @@ -1,58 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using Azure.Storage.Blobs.Models; -using Azure; - -namespace Microsoft.Health.Dicom.Blob.Extensions; - -internal static class BlobStorageErrorExtensions -{ - private static readonly List Customer400ErrorCodes = new List - { - BlobErrorCode.UnsupportedHeader - }; - - private static readonly List Customer401ErrorCodes = new List - { - BlobErrorCode.InvalidAuthenticationInfo, - }; - - private static readonly List Customer403ErrorCodes = new List - { - BlobErrorCode.AuthorizationFailure, - BlobErrorCode.AuthorizationPermissionMismatch, - BlobErrorCode.InsufficientAccountPermissions, - BlobErrorCode.AccountIsDisabled, - BlobErrorCode.InvalidAuthenticationInfo, - "KeyVaultEncryptionKeyNotFound", - "KeyVaultAccessTokenCannotBeAcquired", - "KeyVaultVaultNotFound", - }; - - private static readonly List Customer404ErrorCodes = new List - { - BlobErrorCode.ContainerNotFound, - "FilesystemNotFound", - }; - - private static readonly List Customer409ErrorCodes = new List - { - BlobErrorCode.ContainerBeingDeleted, - BlobErrorCode.ContainerDisabled, - }; - - public static bool IsConnectedStoreCustomerError(this RequestFailedException rfe) - { - return (rfe.Status == 400 && Customer400ErrorCodes.Any(e => e.ToString().Equals(rfe.ErrorCode, StringComparison.OrdinalIgnoreCase))) || - (rfe.Status == 401 && Customer401ErrorCodes.Any(e => e.ToString().Equals(rfe.ErrorCode, StringComparison.OrdinalIgnoreCase))) || - (rfe.Status == 403 && Customer403ErrorCodes.Any(e => e.ToString().Equals(rfe.ErrorCode, StringComparison.OrdinalIgnoreCase))) || - (rfe.Status == 404 && Customer404ErrorCodes.Any(e => e.ToString().Equals(rfe.ErrorCode, StringComparison.OrdinalIgnoreCase))) || - (rfe.Status == 409 && Customer409ErrorCodes.Any(e => e.ToString().Equals(rfe.ErrorCode, StringComparison.OrdinalIgnoreCase))); - } -} diff --git a/src/Microsoft.Health.Dicom.Blob/Features/Export/AzureBlobExportFormatOptions.cs b/src/Microsoft.Health.Dicom.Blob/Features/Export/AzureBlobExportFormatOptions.cs deleted file mode 100644 index 8679e6b3ff..0000000000 --- a/src/Microsoft.Health.Dicom.Blob/Features/Export/AzureBlobExportFormatOptions.cs +++ /dev/null @@ -1,35 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using EnsureThat; -using Microsoft.Health.Dicom.Core.Features.Model; - -namespace Microsoft.Health.Dicom.Blob.Features.Export; - -internal sealed class AzureBlobExportFormatOptions -{ - public string ErrorFile { get; } - - public string FilePattern { get; } - - public Guid OperationId { get; } - - public AzureBlobExportFormatOptions(Guid operationId, string dicomFilePattern, string errorFilePattern) - { - OperationId = operationId; - FilePattern = ExportFilePattern.Parse( - EnsureArg.IsNotNullOrWhiteSpace(dicomFilePattern, nameof(dicomFilePattern)), - ExportPatternPlaceholders.All); - ErrorFile = ExportFilePattern.Format( - ExportFilePattern.Parse( - EnsureArg.IsNotNullOrWhiteSpace(errorFilePattern, nameof(errorFilePattern)), - ExportPatternPlaceholders.Operation), - operationId); - } - - public string GetFilePath(VersionedInstanceIdentifier identifier) - => ExportFilePattern.Format(FilePattern, OperationId, identifier); -} diff --git a/src/Microsoft.Health.Dicom.Blob/Features/Export/AzureBlobExportSink.cs b/src/Microsoft.Health.Dicom.Blob/Features/Export/AzureBlobExportSink.cs deleted file mode 100644 index 145382e62b..0000000000 --- a/src/Microsoft.Health.Dicom.Blob/Features/Export/AzureBlobExportSink.cs +++ /dev/null @@ -1,180 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Concurrent; -using System.Globalization; -using System.IO; -using System.Net; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Azure; -using Azure.Identity; -using Azure.Storage.Blobs; -using Azure.Storage.Blobs.Models; -using Azure.Storage.Blobs.Specialized; -using EnsureThat; -using Microsoft.Health.Blob.Configs; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Export; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Models.Common; -using Microsoft.Health.Dicom.Core.Models.Export; - -namespace Microsoft.Health.Dicom.Blob.Features.Export; - -internal sealed class AzureBlobExportSink : IExportSink -{ - public event EventHandler CopyFailure; - - private readonly IFileStore _source; - private readonly BlobContainerClient _dest; - private readonly ConcurrentQueue _errors; - private readonly AzureBlobExportFormatOptions _output; - private readonly BlobOperationOptions _blobOptions; - private readonly JsonSerializerOptions _jsonOptions; - - private const int BlockSize = 2 * 1024 * 1024; // 2MB - - public AzureBlobExportSink( - IFileStore source, - BlobContainerClient dest, - AzureBlobExportFormatOptions outputOptions, - BlobOperationOptions blobOptions, - JsonSerializerOptions jsonOptions) - { - _source = EnsureArg.IsNotNull(source, nameof(source)); - _dest = EnsureArg.IsNotNull(dest, nameof(source)); - _output = EnsureArg.IsNotNull(outputOptions, nameof(outputOptions)); - _blobOptions = EnsureArg.IsNotNull(blobOptions, nameof(blobOptions)); - _jsonOptions = EnsureArg.IsNotNull(jsonOptions, nameof(jsonOptions)); - _errors = new ConcurrentQueue(); - } - - public async Task CopyAsync(ReadResult value, CancellationToken cancellationToken = default) - { - // TODO: Use new blob SDK for copying block blobs when available - if (value.Failure != null) - { - EnqueueError(value.Failure.Identifier, value.Failure.Exception.Message); - return false; - } - - InstanceMetadata i = value.Instance; - try - { - using Stream sourceStream = await _source.GetStreamingFileAsync( - i.VersionedInstanceIdentifier.Version, - i.VersionedInstanceIdentifier.Partition, - i.InstanceProperties.FileProperties, - cancellationToken); - - BlobClient destBlob = _dest.GetBlobClient(_output.GetFilePath(i.VersionedInstanceIdentifier)); - await destBlob.UploadAsync( - sourceStream, - new BlobUploadOptions { TransferOptions = _blobOptions.Upload }, - cancellationToken); - return true; - } - catch (Exception ex) when (ShouldContinue(ex)) - { - CopyFailure?.Invoke(this, new CopyFailureEventArgs(i.VersionedInstanceIdentifier, ex)); - EnqueueError(DicomIdentifier.ForInstance(i.VersionedInstanceIdentifier), ex.Message); - return false; - } - } - - /// - /// When to continue copying to the destination, skipping this specific file and not retrying. - /// Otherwise, we let the exception be thrown, failing the entire export operation and allowing it to retry. - /// - private static bool ShouldContinue(Exception ex) - { - // continue if the data has been modified in the source as it is likely an issue that won't be fixed by retrying - // and no need to fail an entire operation for an issue with a single file - if (ex is DataStoreRequestFailedException dsrfe && dsrfe.IsExternal && dsrfe.ResponseCode == (int)HttpStatusCode.PreconditionFailed) - return true; - - // don't continue when data store is not available and using external as it may be a transient issue - if (ex is DataStoreException dse && dse.IsExternal) - return false; - - // continue if the issue copying to the destination was not due to the client configuration - if (ex is not RequestFailedException rfe || rfe.Status < 400 || rfe.Status >= 500) - return true; - - return false; - } - - public ValueTask DisposeAsync() - => new ValueTask(FlushAsync()); - - public async Task InitializeAsync(CancellationToken cancellationToken = default) - { - // TODO: Should we create the container if it's not present? - try - { - if (!await _dest.ExistsAsync(cancellationToken)) - { - throw new SinkInitializationFailureException( - string.Format(CultureInfo.CurrentCulture, DicomBlobResource.ContainerDoesNotExist, _dest.Name, _dest.AccountName)); - } - - AppendBlobClient client = _dest.GetAppendBlobClient(_output.ErrorFile); - await client.CreateIfNotExistsAsync(cancellationToken: cancellationToken); - return new Uri(client.Uri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.Unescaped), UriKind.Absolute); - } - catch (AggregateException ae) when (ae.InnerException is RequestFailedException) - { - throw new SinkInitializationFailureException( - string.Format(CultureInfo.CurrentCulture, DicomBlobResource.BlobStorageConnectionFailure, _dest.Name, _dest.AccountName), - ae); - } - catch (AuthenticationFailedException afe) - { - throw new SinkInitializationFailureException( - string.Format(CultureInfo.CurrentCulture, DicomBlobResource.BlobStorageAuthenticateFailure, _dest.Name, _dest.AccountName), - afe); - } - catch (RequestFailedException rfe) - { - throw new SinkInitializationFailureException( - string.Format(CultureInfo.CurrentCulture, DicomBlobResource.BlobStorageRequestFailure, _dest.Name, _dest.AccountName), - rfe); - } - } - - public async Task FlushAsync(CancellationToken cancellationToken = default) - { - AppendBlobClient client = _dest.GetAppendBlobClient(_output.ErrorFile); - - using var buffer = new MemoryStream(BlockSize); - while (!_errors.IsEmpty) - { - // Fill up the buffer - buffer.SetLength(0); - while (buffer.Position < BlockSize && _errors.TryDequeue(out ExportErrorLogEntry entry)) - { - await JsonSerializer.SerializeAsync(buffer, entry, _jsonOptions, cancellationToken); - buffer.WriteByte(10); // '\n' in UTF-8 for normalized line endings across platforms - } - - // Append the block - buffer.Seek(0, SeekOrigin.Begin); - await client.AppendBlockAsync(buffer, cancellationToken: cancellationToken); - } - } - - private void EnqueueError(DicomIdentifier identifier, string message) - => _errors.Enqueue( - new ExportErrorLogEntry - { - Error = message, - Identifier = identifier, - Timestamp = DateTimeOffset.UtcNow, - }); -} diff --git a/src/Microsoft.Health.Dicom.Blob/Features/Export/AzureBlobExportSinkProvider.cs b/src/Microsoft.Health.Dicom.Blob/Features/Export/AzureBlobExportSinkProvider.cs deleted file mode 100644 index 35c34000ba..0000000000 --- a/src/Microsoft.Health.Dicom.Blob/Features/Export/AzureBlobExportSinkProvider.cs +++ /dev/null @@ -1,272 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.ComponentModel.DataAnnotations; -using System.Net.Mime; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Health.Blob.Configs; -using Microsoft.Health.Core.Features.Identity; -using Microsoft.Health.Dicom.Blob.Extensions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Export; -using Microsoft.Health.Dicom.Core.Models; -using Microsoft.Health.Dicom.Core.Models.Export; -using Microsoft.Health.Operations; - -namespace Microsoft.Health.Dicom.Blob.Features.Export; - -internal sealed class AzureBlobExportSinkProvider : ExportSinkProvider -{ - internal const string ClientOptionsName = "Export"; - - public override ExportDestinationType Type => ExportDestinationType.AzureBlob; - - private readonly ISecretStore _secretStore; - private readonly IFileStore _fileStore; - private readonly IExternalCredentialProvider _credentialProvider; - private readonly AzureBlobExportSinkProviderOptions _providerOptions; - private readonly AzureBlobClientOptions _clientOptions; - private readonly BlobOperationOptions _operationOptions; - private readonly JsonSerializerOptions _serializerOptions; - private readonly ILogger _logger; - - public AzureBlobExportSinkProvider( - IFileStore fileStore, - IExternalCredentialProvider credentialProvider, - IOptionsSnapshot providerOptions, - IOptionsSnapshot clientOptions, - IOptionsSnapshot operationOptions, - IOptionsSnapshot serializerOptions, - ILogger logger) - { - _fileStore = EnsureArg.IsNotNull(fileStore, nameof(fileStore)); - _credentialProvider = EnsureArg.IsNotNull(credentialProvider, nameof(credentialProvider)); - _providerOptions = EnsureArg.IsNotNull(providerOptions?.Value, nameof(providerOptions)); - _clientOptions = EnsureArg.IsNotNull(clientOptions?.Get(ClientOptionsName), nameof(clientOptions)); - _operationOptions = EnsureArg.IsNotNull(operationOptions?.Value, nameof(operationOptions)); - _serializerOptions = EnsureArg.IsNotNull(serializerOptions?.Value, nameof(serializerOptions)); - _logger = EnsureArg.IsNotNull(logger, nameof(logger)); - } - - public AzureBlobExportSinkProvider( - ISecretStore secretStore, - IFileStore fileStore, - IExternalCredentialProvider credentialProvider, - IOptionsSnapshot providerOptions, - IOptionsSnapshot clientOptions, - IOptionsSnapshot operationOptions, - IOptionsSnapshot serializerOptions, - ILogger logger) - : this(fileStore, credentialProvider, providerOptions, clientOptions, operationOptions, serializerOptions, logger) - { - // The real Azure Functions runtime/container will use DryIoc for dependency injection - // and will still select this ctor for use, even if no ISecretStore service is configured. - // So instead, we simply allow null in either ctor. - _secretStore = secretStore; - } - - protected override async Task CompleteCopyAsync(AzureBlobExportOptions options, CancellationToken cancellationToken = default) - { - if (_secretStore == null) - { - if (options.Secret != null) - _logger.LogWarning("No secret store has been registered, but a secret was previously configured. Unable to clean up sensitive information."); - } - else if (options.Secret != null) - { - if (await _secretStore.DeleteSecretAsync(options.Secret.Name, cancellationToken)) - _logger.LogInformation("Successfully cleaned up sensitive information from secret store."); - else - _logger.LogWarning("Sensitive information has already been deleted for this operation."); - } - } - - protected override async Task CreateAsync(AzureBlobExportOptions options, Guid operationId, CancellationToken cancellationToken = default) - { - options = await RetrieveSensitiveOptionsAsync(options, cancellationToken); - - return new AzureBlobExportSink( - _fileStore, - options.GetBlobContainerClient(_credentialProvider, _clientOptions), - new AzureBlobExportFormatOptions( - operationId, - AzureBlobExportOptions.DicomFilePattern, - AzureBlobExportOptions.ErrorLogPattern), - _operationOptions, - _serializerOptions); - } - - protected override async Task SecureSensitiveInfoAsync(AzureBlobExportOptions options, Guid operationId, CancellationToken cancellationToken = default) - { - // Clear secrets if it's already set - if (options.Secret != null) - options.Secret = null; - - // Determine whether we need to store any settings in the secret store - BlobSecrets secrets = null; - if (options.BlobContainerUri != null) - { - if (ContainsQueryStringParameter(options.BlobContainerUri, AzureStorageConnection.Uri.Sig)) - secrets = new BlobSecrets { BlobContainerUri = options.BlobContainerUri }; - } - else if (ContainsKey(options.ConnectionString, AzureStorageConnection.SharedAccessSignature)) - { - secrets = new BlobSecrets { ConnectionString = options.ConnectionString }; - } - - // If there is sensitive info, store the secret(s) - if (secrets != null) - { - if (_secretStore == null) - throw new InvalidOperationException(DicomBlobResource.MissingSecretStore); - - string name = operationId.ToString(OperationId.FormatSpecifier); - string version = await _secretStore.SetSecretAsync( - name, - JsonSerializer.Serialize(secrets, _serializerOptions), - MediaTypeNames.Application.Json, - cancellationToken); - - options.BlobContainerUri = null; - options.ConnectionString = null; - options.Secret = new SecretKey { Name = name, Version = version }; - } - - return options; - } - - protected override Task ValidateAsync(AzureBlobExportOptions options, CancellationToken cancellationToken = default) - { - ValidationResult error = null; - - // Using connection strings? - if (options.BlobContainerUri == null) - { - // No connection info? - if (string.IsNullOrWhiteSpace(options.ConnectionString) || string.IsNullOrWhiteSpace(options.BlobContainerName)) - { - error = new ValidationResult(DicomBlobResource.MissingExportBlobConnection); - } - else // Otherwise, validate the connection string - { - if (!IsEmulator(options.ConnectionString) && - ContainsKey(options.ConnectionString, AzureStorageConnection.AccountKey)) - { - // Account keys are not allowed - error = new ValidationResult(DicomBlobResource.AzureStorageAccountKeyUnsupported); - } - else if (!_providerOptions.AllowSasTokens && - ContainsKey(options.ConnectionString, AzureStorageConnection.SharedAccessSignature)) - { - // SAS tokens are not allowed - error = new ValidationResult(DicomBlobResource.SasTokenAuthenticationUnsupported); - } - else if (!_providerOptions.AllowPublicAccess && - !ContainsKey(options.ConnectionString, AzureStorageConnection.SharedAccessSignature)) - { - // Public access not allowed - error = new ValidationResult(DicomBlobResource.PublicBlobStorageConnectionUnsupported); - } - else if (options.UseManagedIdentity) - { - // Managed identity must be used with URIs - error = new ValidationResult(DicomBlobResource.InvalidExportBlobAuthentication); - } - } - } - else // Otherwise, using a blob container URI - { - if (!string.IsNullOrWhiteSpace(options.ConnectionString) || !string.IsNullOrWhiteSpace(options.BlobContainerName)) - { - // Conflicting connection info - error = new ValidationResult(DicomBlobResource.ConflictingExportBlobConnections); - } - else if (options.UseManagedIdentity && - ContainsQueryStringParameter(options.BlobContainerUri, AzureStorageConnection.Uri.Sig)) - { - // Managed identity and SAS both specified - error = new ValidationResult(DicomBlobResource.ConflictingBlobExportAuthentication); - } - else if (!_providerOptions.AllowSasTokens && - ContainsQueryStringParameter(options.BlobContainerUri, AzureStorageConnection.Uri.Sig)) - { - // SAS tokens are not allowed - error = new ValidationResult(DicomBlobResource.SasTokenAuthenticationUnsupported); - } - else if (!_providerOptions.AllowPublicAccess && !options.UseManagedIdentity && - !ContainsQueryStringParameter(options.BlobContainerUri, AzureStorageConnection.Uri.Sig)) - { - // No auth specified, but public access is forbidden - error = new ValidationResult(DicomBlobResource.PublicBlobStorageConnectionUnsupported); - } - } - - return error == null ? Task.CompletedTask : throw new ValidationException(error.ErrorMessage); - } - - private async Task RetrieveSensitiveOptionsAsync(AzureBlobExportOptions options, CancellationToken cancellationToken = default) - { - if (options.Secret != null) - { - if (_secretStore == null) - throw new InvalidOperationException(DicomBlobResource.MissingSecretStore); - - string json = await _secretStore.GetSecretAsync(options.Secret.Name, options.Secret.Version, cancellationToken); - BlobSecrets secrets = JsonSerializer.Deserialize(json, _serializerOptions); - - options.ConnectionString = secrets.ConnectionString; - options.BlobContainerUri = secrets.BlobContainerUri; - options.Secret = null; - } - - return options; - } - - private static bool IsEmulator(string connectionString) - { - EnsureArg.IsNotNullOrWhiteSpace(connectionString, nameof(connectionString)); - return connectionString.Equals("UseDevelopmentStorage=true", StringComparison.OrdinalIgnoreCase) || - ContainsPair(connectionString, AzureStorageConnection.AccountName, "devstoreaccount1"); - } - - // Note: These Contain methods may produce false positives as they do not check for the exact name - - private static bool ContainsKey(string connectionString, string key) - { - EnsureArg.IsNotNullOrWhiteSpace(connectionString, nameof(connectionString)); - EnsureArg.IsNotNullOrWhiteSpace(key, nameof(key)); - - return connectionString.Contains(key + '=', StringComparison.OrdinalIgnoreCase); - } - - private static bool ContainsPair(string connectionString, string key, string value) - { - EnsureArg.IsNotNullOrWhiteSpace(connectionString, nameof(connectionString)); - EnsureArg.IsNotNullOrWhiteSpace(key, nameof(key)); - - return connectionString.Contains(key + '=' + value, StringComparison.OrdinalIgnoreCase); - } - - private static bool ContainsQueryStringParameter(Uri storageUri, string name) - { - EnsureArg.IsNotNull(storageUri, nameof(storageUri)); - EnsureArg.IsNotNullOrWhiteSpace(name, nameof(name)); - - return storageUri.Query.Contains(name + '=', StringComparison.OrdinalIgnoreCase); - } - - private sealed class BlobSecrets - { - public string ConnectionString { get; set; } - - public Uri BlobContainerUri { get; set; } - } -} diff --git a/src/Microsoft.Health.Dicom.Blob/Features/Export/AzureBlobExportSinkProviderOptions.cs b/src/Microsoft.Health.Dicom.Blob/Features/Export/AzureBlobExportSinkProviderOptions.cs deleted file mode 100644 index fc8e43841f..0000000000 --- a/src/Microsoft.Health.Dicom.Blob/Features/Export/AzureBlobExportSinkProviderOptions.cs +++ /dev/null @@ -1,32 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Blob.Features.Export; - -/// -/// Represents the options for the Azure Blob Storage export sink provider. -/// -public class AzureBlobExportSinkProviderOptions -{ - /// - /// The default section name within a configuration. - /// - public const string DefaultSection = "Export:Sinks:AzureBlob"; - - /// - /// Gets or sets whether SAS tokens should be an allowed form of authentication. - /// - /// - /// if SAS tokens can be used in connection strings or URIs; - /// otherwise, - /// - public bool AllowSasTokens { get; set; } - - /// - /// Gets or sets whether the sink allows connections to storage accounts with public access (unauthenticated). - /// - /// if unauthenticated connections are allowed; otherwise, - public bool AllowPublicAccess { get; set; } -} diff --git a/src/Microsoft.Health.Dicom.Blob/Features/Export/ExportFilePattern.cs b/src/Microsoft.Health.Dicom.Blob/Features/Export/ExportFilePattern.cs deleted file mode 100644 index 70a2fef50b..0000000000 --- a/src/Microsoft.Health.Dicom.Blob/Features/Export/ExportFilePattern.cs +++ /dev/null @@ -1,76 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Globalization; -using System.Text; -using EnsureThat; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Operations; - -namespace Microsoft.Health.Dicom.Blob.Features.Export; - -// This class is meant to help translate patterns like "/%Study%/%Series%/%SopInstance%.dcm" -// into format strings like "/{1}/{2}/{3}.dcm" which we can use when exporting. -// This feature is not currently exposed, but could be should the need arise. - -internal static class ExportFilePattern -{ - public static string Parse(string pattern, ExportPatternPlaceholders placeholders = ExportPatternPlaceholders.All) - { - var builder = new StringBuilder(); - for (int i = 0; i < pattern.Length; i++) - { - if (pattern[i] == '\\') - { - if (++i == pattern.Length) - throw new FormatException(string.Format(CultureInfo.CurrentCulture, DicomBlobResource.InvalidEscapeSequence, "\\")); - - if (pattern[i] != '%') - throw new FormatException(string.Format(CultureInfo.CurrentCulture, DicomBlobResource.InvalidEscapeSequence, "\\" + pattern[i])); - - builder.Append(pattern[i]); - } - else if (pattern[i] == '%') - { - builder.Append('{'); - - int j = pattern.IndexOf('%', i + 1); - if (j == -1) - throw new FormatException(DicomBlobResource.MalformedPlaceholder); - - string p = pattern.Substring(i + 1, j - i - 1); - if (placeholders.HasFlag(ExportPatternPlaceholders.Operation) && p.Equals(nameof(ExportPatternPlaceholders.Operation), StringComparison.OrdinalIgnoreCase)) - builder.Append($"0:{OperationId.FormatSpecifier}"); - else if (placeholders.HasFlag(ExportPatternPlaceholders.Study) && p.Equals(nameof(ExportPatternPlaceholders.Study), StringComparison.OrdinalIgnoreCase)) - builder.Append('1'); - else if (placeholders.HasFlag(ExportPatternPlaceholders.Series) && p.Equals(nameof(ExportPatternPlaceholders.Series), StringComparison.OrdinalIgnoreCase)) - builder.Append('2'); - else if (placeholders.HasFlag(ExportPatternPlaceholders.SopInstance) && p.Equals(nameof(ExportPatternPlaceholders.SopInstance), StringComparison.OrdinalIgnoreCase)) - builder.Append('3'); - else - throw new FormatException(string.Format(CultureInfo.CurrentCulture, DicomBlobResource.UnknownPlaceholder, p)); - - builder.Append('}'); - i = j; // Move ahead - } - else - { - builder.Append(pattern[i]); - } - } - - return builder.ToString(); - } - - public static string Format(string format, Guid operationId) - => string.Format(CultureInfo.InvariantCulture, format, operationId); - - public static string Format(string format, Guid operationId, VersionedInstanceIdentifier identifier) - { - EnsureArg.IsNotNull(identifier, nameof(identifier)); - return string.Format(CultureInfo.InvariantCulture, format, operationId, identifier.StudyInstanceUid, identifier.SeriesInstanceUid, identifier.SopInstanceUid); - } -} diff --git a/src/Microsoft.Health.Dicom.Blob/Features/Export/ExportPatternPlaceholder.cs b/src/Microsoft.Health.Dicom.Blob/Features/Export/ExportPatternPlaceholder.cs deleted file mode 100644 index a414704efd..0000000000 --- a/src/Microsoft.Health.Dicom.Blob/Features/Export/ExportPatternPlaceholder.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; - -[Flags] -internal enum ExportPatternPlaceholders -{ - None = 0x0, - Operation = 0x1, - Study = 0x2, - Series = 0x4, - SopInstance = 0x8, - All = Operation | Study | Series | SopInstance, -} diff --git a/src/Microsoft.Health.Dicom.Blob/Features/ExternalStore/ExternalBlobClient.cs b/src/Microsoft.Health.Dicom.Blob/Features/ExternalStore/ExternalBlobClient.cs deleted file mode 100644 index 7e466b8c45..0000000000 --- a/src/Microsoft.Health.Dicom.Blob/Features/ExternalStore/ExternalBlobClient.cs +++ /dev/null @@ -1,127 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using System; -using Azure; -using Azure.Core; -using Azure.Storage.Blobs; -using Azure.Storage.Blobs.Models; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Health.Blob.Configs; -using Microsoft.Health.Dicom.Blob.Features.Storage; -using Microsoft.Health.Dicom.Blob.Utilities; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Core.Features.Identity; -using Microsoft.Health.Dicom.Core.Features.Common; - -namespace Microsoft.Health.Dicom.Blob.Features.ExternalStore; - -/// Represents the blob container created by the user and initialized JIT -internal class ExternalBlobClient : IBlobClient -{ - private readonly object _lockObj = new object(); - private readonly BlobServiceClientOptions _blobClientOptions; - private readonly ExternalBlobDataStoreConfiguration _externalStoreOptions; - private readonly IExternalCredentialProvider _credentialProvider; - private BlobContainerClient _blobContainerClient; - private readonly bool _isPartitionEnabled; - private readonly ILogger _logger; - - /// - /// Configures a blob client for an external store. - /// - /// - /// Options to use with configuring the external store. - /// Options to use when configuring the blob client. - /// Feature configuration. - /// A logger for diagnostic information. - public ExternalBlobClient( - IExternalCredentialProvider credentialProvider, - IOptions externalStoreOptions, - IOptions blobClientOptions, - IOptions featureConfiguration, - ILogger logger) - { - _credentialProvider = EnsureArg.IsNotNull(credentialProvider, nameof(credentialProvider)); - _blobClientOptions = EnsureArg.IsNotNull(blobClientOptions?.Value, nameof(blobClientOptions)); - _externalStoreOptions = EnsureArg.IsNotNull(externalStoreOptions?.Value, nameof(externalStoreOptions)); - _externalStoreOptions.StorageDirectory = SanitizeServiceStorePath(_externalStoreOptions.StorageDirectory); - _isPartitionEnabled = EnsureArg.IsNotNull(featureConfiguration, nameof(featureConfiguration)).Value.EnableDataPartitions; - _logger = EnsureArg.IsNotNull(logger, nameof(logger)); - _logger.LogInformation("External blob client registered. Partition feature flag is set to {IsPartitionEnabled}", _isPartitionEnabled); - - ExternalStoreHealthExpiryHttpPipelinePolicy httpPolicy = new ExternalStoreHealthExpiryHttpPipelinePolicy(_externalStoreOptions); - _blobClientOptions.AddPolicy(httpPolicy, HttpPipelinePosition.PerCall); - } - - public bool IsExternal => true; - - public BlobContainerClient BlobContainerClient - { - get - { - if (_blobContainerClient == null) - { - lock (_lockObj) - { - if (_blobContainerClient == null) - { - try - { - if (_externalStoreOptions.BlobContainerUri != null) - { - TokenCredential credential = _credentialProvider.GetTokenCredential(); - _blobContainerClient = new BlobContainerClient(_externalStoreOptions.BlobContainerUri, credential, _blobClientOptions); - } - else - { - _blobContainerClient = new BlobContainerClient(_externalStoreOptions.ConnectionString, _externalStoreOptions.ContainerName, _blobClientOptions); - } - } - catch (Exception ex) - { - throw new DataStoreException(ex, isExternal: true); - } - } - } - } - return _blobContainerClient; - } - set => _blobContainerClient = value; - } - - private static string SanitizeServiceStorePath(string path) - { - return !path.EndsWith('/') ? path + "/" : path; - } - - /// - /// Gets path to store blobs in. When partitioning is enabled, the path appends partition as a subdirectory. - /// - /// Partition name to use to append as subdirectory to prefix. - /// - public string GetServiceStorePath(string partitionName) - { - return _isPartitionEnabled ? - _externalStoreOptions.StorageDirectory + partitionName + "/" : - _externalStoreOptions.StorageDirectory; - } - - public BlobRequestConditions GetConditions(FileProperties fileProperties) - { - if (fileProperties != null) - { - return new BlobRequestConditions // ensure file has not been changed since we last worked with it - { - IfMatch = new ETag(fileProperties.ETag), - }; - } - - return null; - } -} diff --git a/src/Microsoft.Health.Dicom.Blob/Features/ExternalStore/ExternalStoreHealthExpiryHttpPipelinePolicy.cs b/src/Microsoft.Health.Dicom.Blob/Features/ExternalStore/ExternalStoreHealthExpiryHttpPipelinePolicy.cs deleted file mode 100644 index 5c5f6cac08..0000000000 --- a/src/Microsoft.Health.Dicom.Blob/Features/ExternalStore/ExternalStoreHealthExpiryHttpPipelinePolicy.cs +++ /dev/null @@ -1,72 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Globalization; -using System.IO; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using Azure.Core; -using Azure.Core.Pipeline; -using Azure.Storage.Blobs; -using EnsureThat; -using Microsoft.Health.Dicom.Blob.Utilities; - -namespace Microsoft.Health.Dicom.Blob.Features.ExternalStore; - -internal class ExternalStoreHealthExpiryHttpPipelinePolicy : HttpPipelinePolicy -{ - private readonly ExternalBlobDataStoreConfiguration _externalStoreOptions; - private readonly Regex _healthCheckRegex; - private const string GuidRegex = @"[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}"; - - public ExternalStoreHealthExpiryHttpPipelinePolicy(ExternalBlobDataStoreConfiguration externalStoreOptions) - { - _externalStoreOptions = EnsureArg.IsNotNull(externalStoreOptions, nameof(externalStoreOptions)); - EnsureArg.IsNotNull(externalStoreOptions.StorageDirectory, nameof(externalStoreOptions.StorageDirectory)); - EnsureArg.IsNotNull(externalStoreOptions.HealthCheckFilePath, nameof(externalStoreOptions.HealthCheckFilePath)); - - Uri blobUri; - - if (_externalStoreOptions.BlobContainerUri != null) - { - blobUri = _externalStoreOptions.BlobContainerUri; - } - else - { - // For local testing with Azurite - BlobContainerClient blobContainerClient = new BlobContainerClient(_externalStoreOptions.ConnectionString, _externalStoreOptions.ContainerName); - blobUri = blobContainerClient.Uri; - } - - UriBuilder uriBuilder = new UriBuilder(blobUri); - uriBuilder.Path = Path.Combine(uriBuilder.Path, _externalStoreOptions.StorageDirectory, _externalStoreOptions.HealthCheckFilePath); - - string healthCheckPathRegex = Regex.Escape(uriBuilder.Uri.AbsoluteUri); - _healthCheckRegex = new Regex($"^{healthCheckPathRegex}{GuidRegex}\\.txt$", RegexOptions.CultureInvariant | RegexOptions.Compiled); - } - - public override void Process(HttpMessage message, ReadOnlyMemory pipeline) - { - AddExpiryHeaderToHealthCheckFileUploadRequest(message); - ProcessNext(message, pipeline); - } - - public override ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory pipeline) - { - AddExpiryHeaderToHealthCheckFileUploadRequest(message); - return ProcessNextAsync(message, pipeline); - } - - private void AddExpiryHeaderToHealthCheckFileUploadRequest(HttpMessage message) - { - if (_healthCheckRegex.IsMatch(message.Request.Uri.ToUri().AbsoluteUri) && - (message.Request.Method == RequestMethod.Put || message.Request.Method == RequestMethod.Post || message.Request.Method == RequestMethod.Patch)) - { - message.Request.Headers.Add("x-ms-expiry-time", _externalStoreOptions.HealthCheckFileExpiry.TotalMilliseconds.ToString(CultureInfo.InvariantCulture)); - message.Request.Headers.Add("x-ms-expiry-option", "RelativeToNow"); - } - } -} diff --git a/src/Microsoft.Health.Dicom.Blob/Features/Health/DicomBlobHealthCheck.cs b/src/Microsoft.Health.Dicom.Blob/Features/Health/DicomBlobHealthCheck.cs deleted file mode 100644 index 2fcf3e1682..0000000000 --- a/src/Microsoft.Health.Dicom.Blob/Features/Health/DicomBlobHealthCheck.cs +++ /dev/null @@ -1,49 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Azure.Storage.Blobs; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Health.Blob.Configs; -using Microsoft.Health.Blob.Features.Health; -using Microsoft.Health.Blob.Features.Storage; -using Microsoft.Health.Core.Features.Health; -using Microsoft.Health.Dicom.Blob.Utilities; -using Microsoft.Health.Encryption.Customer.Health; - -namespace Microsoft.Health.Dicom.Blob.Features.Health; - -/// -/// Checks for the DICOM blob service health. -/// -public class DicomBlobHealthCheck : BlobHealthCheck - where TStoreConfigurationSection : IStoreConfigurationSection -{ - /// - /// Initializes a new instance of the class. - /// - /// The blob client factory. - /// The IOptions accessor to get a named blob container version. - /// - /// The test provider. - /// The cached result of the customer key health status. - /// The logger. - public DicomBlobHealthCheck( - BlobServiceClient client, - IOptionsSnapshot namedBlobContainerConfigurationAccessor, - TStoreConfigurationSection storeConfigurationSection, - IBlobClientTestProvider testProvider, - ValueCache customerKeyHealthCache, - ILogger> logger) - : base( - client, - namedBlobContainerConfigurationAccessor, - storeConfigurationSection.ContainerConfigurationName, - testProvider, - customerKeyHealthCache, - logger) - { - } -} diff --git a/src/Microsoft.Health.Dicom.Blob/Features/Health/DicomConnectedStoreHealthCheck.cs b/src/Microsoft.Health.Dicom.Blob/Features/Health/DicomConnectedStoreHealthCheck.cs deleted file mode 100644 index ccf28fc597..0000000000 --- a/src/Microsoft.Health.Dicom.Blob/Features/Health/DicomConnectedStoreHealthCheck.cs +++ /dev/null @@ -1,113 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Azure; -using Azure.Storage.Blobs; -using Azure.Storage.Blobs.Models; -using Azure.Storage.Blobs.Specialized; -using EnsureThat; -using Microsoft.Extensions.Diagnostics.HealthChecks; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Health.Core.Features.Health; -using Microsoft.Health.Dicom.Blob.Extensions; -using Microsoft.Health.Dicom.Blob.Features.Storage; -using Microsoft.Health.Dicom.Blob.Utilities; -using Microsoft.Health.Dicom.Core.Extensions; - -namespace Microsoft.Health.Dicom.Blob.Features.Health; - -internal class DicomConnectedStoreHealthCheck : IHealthCheck -{ - private readonly string _degradedDescription = "The health of the connected store has degraded."; - private readonly string _testContent = "Test content."; - - private readonly ExternalBlobDataStoreConfiguration _externalStoreOptions; - private readonly IBlobClient _blobClient; - private readonly ILogger _logger; - - /// - /// Validate health of connected blob store - /// - /// the blob client - /// external store options - /// logger - public DicomConnectedStoreHealthCheck(IBlobClient blobClient, IOptions externalStoreOptions, ILogger logger) - { - _externalStoreOptions = EnsureArg.IsNotNull(externalStoreOptions?.Value, nameof(externalStoreOptions)); - _blobClient = EnsureArg.IsNotNull(blobClient, nameof(blobClient)); - _logger = EnsureArg.IsNotNull(logger, nameof(logger)); - } - - public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) - { - _logger.LogInformation("Checking the health of the connected store."); - - BlobContainerClient containerClient = _blobClient.BlobContainerClient; - BlockBlobClient healthCheckBlobClient = containerClient.GetBlockBlobClient(Path.Combine(_externalStoreOptions.StorageDirectory, $"{_externalStoreOptions.HealthCheckFilePath}{Guid.NewGuid()}.txt")); - - _logger.LogInformation("Attempting to write, read, and delete file {FileName}.", healthCheckBlobClient.Name); - - try - { - using Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(_testContent)); - - // test blob/write - await healthCheckBlobClient.UploadAsync(stream, cancellationToken: cancellationToken); - - // test blob/read and blob/metadata/read - await healthCheckBlobClient.DownloadContentAsync(cancellationToken); - - // test blob/delete - await healthCheckBlobClient.DeleteAsync(DeleteSnapshotsOption.IncludeSnapshots, new BlobRequestConditions(), cancellationToken); - - return HealthCheckResult.Healthy("Successfully connected."); - } - catch (RequestFailedException rfe) when (rfe.IsConnectedStoreCustomerError()) - { - return GetConnectedStoreDegradedResult(rfe); - } - catch (Exception ex) when (ex.IsStorageAccountUnknownHostError()) - { - return GetConnectedStoreDegradedResult(ex); - } - finally - { - // Remove in WI #114591 - await TryDeleteOldBlob(containerClient); - } - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Do not fail on clean up")] - private async Task TryDeleteOldBlob(BlobContainerClient blobContainerClient) - { - try - { - BlockBlobClient blockBlobClient = blobContainerClient.GetBlockBlobClient($"{_externalStoreOptions.StorageDirectory}healthCheck/health.txt"); - await blockBlobClient.DeleteIfExistsAsync(DeleteSnapshotsOption.IncludeSnapshots); - } - catch (Exception) - { - // do not throw if cleaning up the previous blob fails - } - } - - private HealthCheckResult GetConnectedStoreDegradedResult(Exception exception) - { - _logger.LogInformation(exception, "The connected store health check failed due to a client issue."); - - return new HealthCheckResult( - HealthStatus.Degraded, - _degradedDescription, - exception, - new Dictionary { { "Reason", HealthStatusReason.ConnectedStoreDegraded.ToString() } }); - } -} diff --git a/src/Microsoft.Health.Dicom.Blob/Features/Storage/BlobFileStore.cs b/src/Microsoft.Health.Dicom.Blob/Features/Storage/BlobFileStore.cs deleted file mode 100644 index 9034e2b619..0000000000 --- a/src/Microsoft.Health.Dicom.Blob/Features/Storage/BlobFileStore.cs +++ /dev/null @@ -1,613 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Buffers; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Azure; -using Azure.Storage.Blobs.Models; -using Azure.Storage.Blobs.Specialized; -using EnsureThat; -using Microsoft.ApplicationInsights; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Health.Blob.Configs; -using Microsoft.Health.Dicom.Blob.Features.Telemetry; -using Microsoft.Health.Dicom.Core; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Diagnostic; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Features.Partitioning; - -namespace Microsoft.Health.Dicom.Blob.Features.Storage; - -/// -/// Provides functionality for managing the DICOM files using the Azure Blob storage. -/// -public class BlobFileStore : IFileStore -{ - private readonly BlobOperationOptions _options; - private readonly ILogger _logger; - private readonly IBlobClient _blobClient; - private readonly DicomFileNameWithPrefix _nameWithPrefix; - private readonly BlobFileStoreMeter _blobFileStoreMeter; - private readonly TelemetryClient _telemetryClient; - - private static readonly Action LogBlobClientOperationDelegate = - LoggerMessage.Define( - LogLevel.Information, - default, - "Operation '{OperationName}' processed. Using external store {IsExternalStore}."); - - private static readonly Action LogBlobClientOperationWithStreamDelegate = - LoggerMessage.Define( - LogLevel.Information, - default, - "Operation '{OperationName}' processed stream length '{StreamLength}'."); - - public BlobFileStore( - IBlobClient blobClient, - DicomFileNameWithPrefix nameWithPrefix, - IOptions options, - BlobFileStoreMeter blobFileStoreMeter, - ILogger logger, - TelemetryClient telemetryClient) - { - _nameWithPrefix = EnsureArg.IsNotNull(nameWithPrefix, nameof(nameWithPrefix)); - _options = EnsureArg.IsNotNull(options?.Value, nameof(options)); - _logger = EnsureArg.IsNotNull(logger, nameof(logger)); - _blobClient = EnsureArg.IsNotNull(blobClient, nameof(blobClient)); - _blobFileStoreMeter = EnsureArg.IsNotNull(blobFileStoreMeter, nameof(blobFileStoreMeter)); - _telemetryClient = EnsureArg.IsNotNull(telemetryClient, nameof(telemetryClient)); - } - - /// - public Task StoreFileAsync( - long version, - string partitionName, - Stream stream, - CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(stream, nameof(stream)); - - BlockBlobClient blobClient = GetNewInstanceBlockBlobClient(version, partitionName); - - var blobUploadOptions = new BlobUploadOptions { TransferOptions = _options.Upload }; - stream.Seek(0, SeekOrigin.Begin); - - return ExecuteAsync( - func: async () => - { - BlobContentInfo info = await blobClient.UploadAsync(stream, blobUploadOptions, cancellationToken); - return new FileProperties - { - Path = blobClient.Name, - ETag = info.ETag.ToString(), - ContentLength = stream.Length, - }; - }, - operationName: nameof(StoreFileAsync), - operationType: OperationType.Input, - extractLength: long? (newBlobFileProperties) => newBlobFileProperties.ContentLength); - } - - /// - public Task StoreFileInBlocksAsync( - long version, - Partition partition, - Stream stream, - int stageBlockSizeInBytes, - KeyValuePair firstBlock, - CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(stream, nameof(stream)); - EnsureArg.IsNotNull(partition, nameof(partition)); - - BlockBlobClient blobClient = GetNewInstanceBlockBlobClient(version, partition.Name); - - return ExecuteAsync( - func: async () => - { - byte[] buffer = ArrayPool.Shared.Rent(Math.Min(stageBlockSizeInBytes, (int)stream.Length)); - FileProperties fileProperties; - KeyValuePair block = default; - long totalBytesRead = 0; - int blockIteration = 0; - List blockIds = new(); - - try - { - while (totalBytesRead < stream.Length) - { - // Create a new block with a unique key and a size equal to the minimum value between stageBlockSizeInBytes and the remaining bytes in the stream - // If its a first block use the passed value since it is pre-determined and rest are dependent on the stream readAsync. The first block will contain the patient metadata. - block = blockIteration == 0 ? firstBlock : new KeyValuePair(Convert.ToBase64String(Guid.NewGuid().ToByteArray()), Math.Min(stageBlockSizeInBytes, stream.Length - totalBytesRead)); - - // Read data from the input stream into the buffer array. Azure.Sorage.LazyLoadingReadOnlyStream may not return the full amount of bytes requested, so we need to loop until we have read the full amount. -#pragma warning disable CA1835 // Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync' - long bytesRead = await stream.ReadAsync(buffer, 0, (int)block.Value, cancellationToken); -#pragma warning restore CA1835 // Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync' - - // Create a MemoryStream that wraps the buffer array and contains only the data read in the current iteration - using var blockStream = new MemoryStream(buffer, 0, (int)bytesRead); - - // Stage the block with the specified key and data from the blockStream - await blobClient.StageBlockAsync(block.Key, blockStream, cancellationToken: cancellationToken); - - blockIteration++; - totalBytesRead += bytesRead; - blockIds.Add(block.Key); - } - - // Commit the blocks that are staged in the blob - BlobContentInfo info = await blobClient.CommitBlockListAsync( - blockIds, - cancellationToken: cancellationToken); - - fileProperties = new FileProperties - { - Path = blobClient.Name, - ETag = info.ETag.ToString(), - ContentLength = stream.Length, - }; - } - finally - { - ArrayPool.Shared.Return(buffer); - } - - return fileProperties; - }, - operationName: nameof(StoreFileInBlocksAsync), - operationType: OperationType.Input, - extractLength: long? (newBlobFileProperties) => newBlobFileProperties.ContentLength); - } - - /// - public Task UpdateFileBlockAsync( - long version, - Partition partition, - FileProperties fileProperties, - string blockId, - Stream stream, - CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(stream, nameof(stream)); - EnsureArg.IsNotNull(partition, nameof(partition)); - EnsureArg.IsNotNullOrWhiteSpace(blockId, nameof(blockId)); - - return ExecuteAsync( - func: async () => - { - BlockBlobClient blobClient = GetExistingInstanceBlockBlobClient(version, partition, fileProperties); - _logger.LogInformation( - "Trying to read block list for DICOM instance file with version '{Version}'.", - version); - - BlockList blockList = await blobClient.GetBlockListAsync( - BlockListTypes.Committed, - snapshot: null, - conditions: null, // GetBlockListAsync does not support IfMatch conditions to check eTag - cancellationToken); - - IEnumerable blockIds = blockList.CommittedBlocks.Select(x => x.Name); - - string blockToUpdate = blockIds.FirstOrDefault(x => x.Equals(blockId, StringComparison.OrdinalIgnoreCase)); - - if (blockToUpdate == null) - throw new DataStoreException(DicomBlobResource.BlockNotFound, null, _blobClient.IsExternal); - - stream.Seek(0, SeekOrigin.Begin); - await blobClient.StageBlockAsync(blockId, stream, cancellationToken: cancellationToken); - BlobContentInfo info = await blobClient.CommitBlockListAsync( - blockIds, - cancellationToken: cancellationToken); - return new FileProperties - { - Path = blobClient.Name, - ETag = info.ETag.ToString(), - ContentLength = stream.Length - }; - }, - operationName: nameof(UpdateFileBlockAsync), - operationType: OperationType.Input, - extractLength: long? (newBlobFileProperties) => newBlobFileProperties.ContentLength); - } - - /// - public async Task DeleteFileIfExistsAsync( - long version, - Partition partition, - FileProperties fileProperties, - CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(partition); - - BlockBlobClient blobClient = GetExistingInstanceBlockBlobClient(version, partition, fileProperties); - _logger.LogInformation( - "Trying to delete DICOM instance file with watermark: '{Version}' and PartitionKey: {PartitionKey}.", - version, - partition.Key); - - await ExecuteAsync( - func: async () => - { - try - { - // NOTE - when file does not exist but conditions passed in, it fails on conditions not met - return await blobClient.DeleteIfExistsAsync( - DeleteSnapshotsOption.IncludeSnapshots, - conditions: _blobClient.GetConditions(fileProperties), - cancellationToken); - } - catch (RequestFailedException ex) when (ex.ErrorCode == BlobErrorCode.ConditionNotMet && _blobClient.IsExternal) - { - string message = string.Format( - CultureInfo.InvariantCulture, - DicomCoreResource.ExternalDataStoreBlobModified, - ex.ErrorCode, - "delete", - fileProperties.Path, - fileProperties.ETag); - - _telemetryClient.ForwardLogTrace(message, partition, fileProperties, ApplicationInsights.DataContracts.SeverityLevel.Warning); - - _logger.LogInformation( - "Can not delete blob in external store as it has changed or been deleted. File from watermark: '{Version}' and PartitionKey: {PartitionKey}. Dangling SQL Index detected. Will not retry", - version, - partition.Key); - } - - return null; - }, - operationName: nameof(DeleteFileIfExistsAsync), - operationType: OperationType.Input); - } - - /// - public Task GetFileAsync( - long version, - Partition partition, - FileProperties fileProperties, - CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(partition, nameof(partition)); - - BlockBlobClient blobClient = GetExistingInstanceBlockBlobClient(version, partition, fileProperties); - - var blobOpenReadOptions = new BlobOpenReadOptions(allowModifications: false); - blobOpenReadOptions.Conditions = _blobClient.GetConditions(fileProperties); - _logger.LogInformation("Trying to read DICOM instance file with watermark '{Version}'.", version); - // todo: RetrievableStream is returned with no Stream.Length implement which will throw when parsing using fo-dicom for transcoding and frame retrieved. - // We should either remove fo-dicom parsing for transcoding or make SDK change to support Length property on RetrievableStream - //Response result = await blobClient.DownloadStreamingAsync(range: default, conditions: null, rangeGetContentHash: false, cancellationToken); - //stream = result.Value.Content; - return ExecuteAsync( - func: () => blobClient.OpenReadAsync(blobOpenReadOptions, cancellationToken), - operationName: nameof(GetFileAsync), - operationType: OperationType.Output, - extractLength: long? (stream) => stream.Length); - } - - /// - public async Task GetStreamingFileAsync( - long version, - Partition partition, - FileProperties fileProperties, - CancellationToken cancellationToken) - { - BlockBlobClient blobClient = GetExistingInstanceBlockBlobClient(version, partition, fileProperties); - - _logger.LogInformation("Trying to read DICOM instance file with watermark '{Version}'.", version); - - BlobDownloadStreamingResult result = await ExecuteAsync( - func: async () => - { - Response result = await blobClient.DownloadStreamingAsync( - range: default, - conditions: _blobClient.GetConditions(fileProperties), - rangeGetContentHash: false, - cancellationToken); - return result.Value; - }, - operationName: nameof(GetStreamingFileAsync), - operationType: OperationType.Output, - extractLength: long? (result) => result.Details.ContentLength); - - return result.Content; - } - - /// - public Task GetFilePropertiesAsync( - long version, - Partition partition, - FileProperties fileProperties, - CancellationToken cancellationToken) - { - BlockBlobClient blobClient = GetExistingInstanceBlockBlobClient(version, partition, fileProperties); - _logger.LogInformation("Trying to read DICOM instance fileProperties with watermark '{Version}'.", version); - - return ExecuteAsync( - func: async () => - { - BlobProperties blobProperties = await blobClient.GetPropertiesAsync( - conditions: _blobClient.GetConditions(fileProperties), - cancellationToken); - - return new FileProperties - { - Path = blobClient.Name, - ETag = blobProperties.ETag.ToString(), - ContentLength = blobProperties.ContentLength, - }; - }, - operationName: nameof(GetFilePropertiesAsync), - operationType: OperationType.Output); - } - - /// - public async Task GetFileFrameAsync( - long version, - Partition partition, - FrameRange range, - FileProperties fileProperties, - CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(range, nameof(range)); - - BlockBlobClient blob = GetExistingInstanceBlockBlobClient(version, partition, fileProperties); - _logger.LogInformation( - "Trying to read DICOM instance file with version '{Version}' on range {Offset}-{Length}.", - version, - range.Offset, - range.Length); - - BlobDownloadStreamingResult result = await ExecuteAsync( - func: async () => - { - Response result = await blob.DownloadStreamingAsync( - range: new HttpRange(range.Offset, range.Length), - conditions: _blobClient.GetConditions(fileProperties), - rangeGetContentHash: false, - cancellationToken); - return result.Value; - }, - operationName: nameof(GetFileFrameAsync), - operationType: OperationType.Output, - extractLength: long? (result) => result.Details.ContentLength); - - return result.Content; - } - - /// - public async Task GetFileContentInRangeAsync( - long version, - Partition partition, - FileProperties fileProperties, - FrameRange range, - CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(range, nameof(range)); - EnsureArg.IsNotNull(partition, nameof(partition)); - BlockBlobClient blob = GetExistingInstanceBlockBlobClient(version, partition, fileProperties); - _logger.LogInformation( - "Trying to read DICOM instance fileContent with version '{Version}' on range {Offset}-{Length}.", - version, - range.Offset, - range.Length); - - var blobDownloadOptions = new BlobDownloadOptions - { - Range = new HttpRange(range.Offset, range.Length), - Conditions = _blobClient.GetConditions(fileProperties), - }; - - BlobDownloadResult result = await ExecuteAsync( - func: async () => - { - Response result = await blob.DownloadContentAsync(blobDownloadOptions, cancellationToken); - return result.Value; - }, - operationName: nameof(GetFileContentInRangeAsync), - operationType: OperationType.Output, - extractLength: long? (result) => result.Details.ContentLength); - - return result.Content; - } - - /// - public Task> GetFirstBlockPropertyAsync( - long version, - Partition partition, - FileProperties fileProperties, - CancellationToken cancellationToken = default) - { - EnsureArg.IsNotNull(partition, nameof(partition)); - BlockBlobClient blobClient = GetExistingInstanceBlockBlobClient(version, partition, fileProperties); - _logger.LogInformation("Trying to read DICOM instance file with version '{Version}' firstBlock.", version); - - return ExecuteAsync( - func: async () => - { - BlockList blockList = await blobClient.GetBlockListAsync( - BlockListTypes.Committed, - snapshot: null, - conditions: null, // GetBlockListAsync does not support IfMatch conditions to check eTag - cancellationToken); - - if (!blockList.CommittedBlocks.Any()) - throw new DataStoreException(DicomBlobResource.BlockListNotFound, null, _blobClient.IsExternal); - - BlobBlock firstBlock = blockList.CommittedBlocks.First(); - return new KeyValuePair(firstBlock.Name, firstBlock.Size); - }, - operationName: nameof(GetFirstBlockPropertyAsync), - operationType: OperationType.Output); - } - - /// - public Task CopyFileAsync( - long originalVersion, - long newVersion, - Partition partition, - FileProperties fileProperties, - CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(partition, nameof(partition)); - var blobClient = GetExistingInstanceBlockBlobClient(originalVersion, partition, fileProperties); - var copyBlobClient = GetNewInstanceBlockBlobClient(newVersion, partition.Name); - _logger.LogInformation( - "Trying to copy DICOM instance file from original version '{Version}' to new path with new version'{NewVersion}'.", - originalVersion, - newVersion); - - return ExecuteAsync( - func: async () => - { - BlobCopyFromUriOptions options = new BlobCopyFromUriOptions(); - options.SourceConditions = _blobClient.GetConditions(fileProperties); - - if (!await copyBlobClient.ExistsAsync(cancellationToken)) - { - _logger.LogInformation( - "Operation {OperationName} processed within CopyFileAsync.", - "ExistsAsync"); - var operation = await copyBlobClient.StartCopyFromUriAsync(blobClient.Uri, options: options, cancellationToken); - await operation.WaitForCompletionAsync(cancellationToken); - return true; - } - - return false; - }, - operationName: nameof(CopyFileAsync), - operationType: OperationType.Input); - } - - /// - public Task SetBlobToColdAccessTierAsync( - long version, - Partition partition, - FileProperties fileProperties, - CancellationToken cancellationToken = default) - { - EnsureArg.IsNotNull(partition, nameof(partition)); - BlockBlobClient blobClient = GetExistingInstanceBlockBlobClient(version, partition, fileProperties); - _logger.LogInformation("Trying to set blob tier for DICOM instance file with watermark '{Version}'.", version); - - return ExecuteAsync( - func: () => blobClient.SetAccessTierAsync( - AccessTier.Cold, - conditions: null, // SetAccessTierAsync does not support matching on etag - cancellationToken: cancellationToken), - operationName: nameof(SetBlobToColdAccessTierAsync), - operationType: OperationType.Input); - } - - /// - /// Gets client based on watermark/version and partition. - /// - /// Version of file to get - /// Partition within which the file should live in - /// Do not use for any *existing* file. Only use for new files which may not already have file properties - /// associated with them. - protected virtual BlockBlobClient GetNewInstanceBlockBlobClient(long version, string partitionName) - { - string blobName = _nameWithPrefix.GetInstanceFileName(version); - string fullPath = _blobClient.GetServiceStorePath(partitionName) + blobName; - // does not throw, just appends uri with blobName - return _blobClient.BlobContainerClient.GetBlockBlobClient(fullPath); - } - - /// - /// Get client based on watermark/version and partition or file properties. - /// - /// Version of file to get - /// Partition within which the file should live in - /// File properties to use for external store. If not using external store, set to null. - /// Always use on existing files whether using for external or internal store. - protected virtual BlockBlobClient GetExistingInstanceBlockBlobClient( - long version, - Partition partition, - FileProperties fileProperties) - { - EnsureArg.IsNotNull(partition, nameof(partition)); - if (_blobClient.IsExternal && fileProperties != null) - { - // does not throw, just appends uri with blobName - return _blobClient.BlobContainerClient.GetBlockBlobClient(fileProperties.Path); - } - - string blobName = _nameWithPrefix.GetInstanceFileName(version); - string fullPath = _blobClient.GetServiceStorePath(partition.Name) + blobName; - // does not throw, just appends uri with blobName - return _blobClient.BlobContainerClient.GetBlockBlobClient(fullPath); - } - - private async Task ExecuteAsync( - Func> func, - string operationName, - OperationType operationType, - Func extractLength = null) - { - try - { - var resp = await func(); - EmitTelemetry(operationName, operationType, extractLength?.Invoke(resp)); - return resp; - } - catch (RequestFailedException ex) when (ex.ErrorCode == BlobErrorCode.BlobNotFound && !_blobClient.IsExternal) - { - _logger.LogError(ex, message: "Access to storage account failed with ErrorCode: {ErrorCode}", ex.ErrorCode); - throw new ItemNotFoundException(ex); - } - catch (RequestFailedException ex) - { - _logger.LogError(ex, message: "Access to storage account failed with ErrorCode: {ErrorCode}", ex.ErrorCode); - throw new DataStoreRequestFailedException(ex, _blobClient.IsExternal); - } - catch (AggregateException ex) when (_blobClient.IsExternal && ex.InnerException is RequestFailedException) - { - var innerEx = ex.InnerException as RequestFailedException; - _logger.LogError(innerEx, - message: "Access to external storage account failed with ErrorCode: {ErrorCode}", - innerEx.ErrorCode); - throw new DataStoreRequestFailedException(innerEx, _blobClient.IsExternal); - } - catch (Exception ex) - { - _logger.LogError(ex, "Access to storage account failed"); - throw new DataStoreException(ex, _blobClient.IsExternal); - } - } - - private void EmitTelemetry(string operationName, OperationType operationType, long? streamLength = null) - { - _blobFileStoreMeter.BlobFileStoreOperationCount.Add( - 1, - BlobFileStoreMeter.CreateTelemetryDimension(operationName, operationType, _blobClient.IsExternal)); - - if (streamLength == null) - { - LogBlobClientOperationDelegate(_logger, operationName, _blobClient.IsExternal, null); - } - else - { - var length = streamLength.Value; - LogBlobClientOperationWithStreamDelegate(_logger, operationName, streamLength.Value, null); - _blobFileStoreMeter.BlobFileStoreOperationStreamSize.Add( - length, - BlobFileStoreMeter.CreateTelemetryDimension( - operationName, - operationType, - _blobClient.IsExternal)); - } - } -} diff --git a/src/Microsoft.Health.Dicom.Blob/Features/Storage/BlobMetadataStore.cs b/src/Microsoft.Health.Dicom.Blob/Features/Storage/BlobMetadataStore.cs deleted file mode 100644 index 403480c14e..0000000000 --- a/src/Microsoft.Health.Dicom.Blob/Features/Storage/BlobMetadataStore.cs +++ /dev/null @@ -1,261 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.IO; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Azure; -using Azure.Storage.Blobs; -using Azure.Storage.Blobs.Models; -using Azure.Storage.Blobs.Specialized; -using EnsureThat; -using FellowOakDicom; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Health.Blob.Configs; -using Microsoft.Health.Dicom.Blob.Features.Telemetry; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Web; -using Microsoft.IO; -using NotSupportedException = System.NotSupportedException; - -namespace Microsoft.Health.Dicom.Blob.Features.Storage; - -/// -/// Provides functionality for managing the DICOM instance metadata. -/// -public class BlobMetadataStore : IMetadataStore -{ - private readonly BlobContainerClient _container; - private readonly JsonSerializerOptions _jsonSerializerOptions; - private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager; - private readonly DicomFileNameWithPrefix _nameWithPrefix; - private readonly ILogger _logger; - private readonly BlobStoreMeter _blobStoreMeter; - private readonly BlobRetrieveMeter _blobRetrieveMeter; - - public BlobMetadataStore( - BlobServiceClient client, - RecyclableMemoryStreamManager recyclableMemoryStreamManager, - DicomFileNameWithPrefix nameWithPrefix, - IOptionsMonitor namedBlobContainerConfigurationAccessor, - IOptions jsonSerializerOptions, - BlobStoreMeter blobStoreMeter, - BlobRetrieveMeter blobRetrieveMeter, - ILogger logger) - { - EnsureArg.IsNotNull(client, nameof(client)); - _jsonSerializerOptions = EnsureArg.IsNotNull(jsonSerializerOptions?.Value, nameof(jsonSerializerOptions)); - _nameWithPrefix = EnsureArg.IsNotNull(nameWithPrefix, nameof(nameWithPrefix)); - EnsureArg.IsNotNull(namedBlobContainerConfigurationAccessor, nameof(namedBlobContainerConfigurationAccessor)); - _recyclableMemoryStreamManager = EnsureArg.IsNotNull(recyclableMemoryStreamManager, nameof(recyclableMemoryStreamManager)); - _logger = EnsureArg.IsNotNull(logger, nameof(logger)); - _blobStoreMeter = EnsureArg.IsNotNull(blobStoreMeter, nameof(blobStoreMeter)); - _blobRetrieveMeter = EnsureArg.IsNotNull(blobRetrieveMeter, nameof(blobRetrieveMeter)); - - BlobContainerConfiguration containerConfiguration = namedBlobContainerConfigurationAccessor - .Get(BlobConstants.MetadataContainerConfigurationName); - - _container = client.GetBlobContainerClient(containerConfiguration.ContainerName); - } - - /// - public async Task StoreInstanceMetadataAsync( - DicomDataset dicomDataset, - long version, - CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(dicomDataset, nameof(dicomDataset)); - - // Creates a copy of the dataset with bulk data removed. - DicomDataset dicomDatasetWithoutBulkData = dicomDataset.CopyWithoutBulkDataItems(); - - BlockBlobClient blobClient = GetInstanceBlockBlobClient(version); - - try - { - await using RecyclableMemoryStream stream = _recyclableMemoryStreamManager.GetStream(tag: nameof(StoreInstanceMetadataAsync)); - await JsonSerializer.SerializeAsync(stream, dicomDatasetWithoutBulkData, _jsonSerializerOptions, cancellationToken); - - stream.Seek(0, SeekOrigin.Begin); - await blobClient.UploadAsync( - stream, - new BlobHttpHeaders { ContentType = KnownContentTypes.ApplicationJsonUtf8 }, - metadata: null, - conditions: null, - accessTier: null, - progressHandler: null, - cancellationToken); - } - catch (Exception ex) - { - if (ex is NotSupportedException) - { - _blobStoreMeter.JsonSerializationException.Add(1, new[] { new KeyValuePair("ExceptionType", ex.GetType().FullName) }); - } - throw new DataStoreException(ex); - } - } - - /// - public async Task DeleteInstanceMetadataIfExistsAsync(long version, CancellationToken cancellationToken) - { - BlockBlobClient blobClient = GetInstanceBlockBlobClient(version); - - await ExecuteAsync(t => blobClient.DeleteIfExistsAsync(DeleteSnapshotsOption.IncludeSnapshots, conditions: null, t), cancellationToken); - } - - /// - public async Task GetInstanceMetadataAsync(long version, CancellationToken cancellationToken) - { - try - { - BlockBlobClient blobClient = GetInstanceBlockBlobClient(version); - return await ExecuteAsync(async t => - { - // TODO: When the JsonConverter for DicomDataset does not need to Seek, we can use DownloadStreaming instead - BlobDownloadResult result = await blobClient.DownloadContentAsync(t); - - // DICOM metadata file includes UTF-8 encoding with BOM and there is a bug with the - // BinaryData.ToObjectFromJson method as seen in this issue: https://github.com/dotnet/runtime/issues/71447 - return await JsonSerializer.DeserializeAsync(result.Content.ToStream(), _jsonSerializerOptions, t); - }, cancellationToken); - } - catch (Exception ex) - { - switch (ex) - { - case ItemNotFoundException: - _logger.LogWarning( - ex, - "The DICOM instance metadata file with watermark '{Version}' does not exist.", - version); - break; - case JsonException or NotSupportedException: - _blobRetrieveMeter.JsonDeserializationException.Add(1, new[] { new KeyValuePair("JsonDeserializationExceptionTypeDimension", ex.GetType().FullName) }); - break; - } - - throw; - } - } - - /// - public async Task DeleteInstanceFramesRangeAsync(long version, CancellationToken cancellationToken) - { - BlockBlobClient blobClient = GetInstanceFramesRangeBlobClient(version); - - await ExecuteAsync(t => blobClient.DeleteIfExistsAsync(DeleteSnapshotsOption.IncludeSnapshots, conditions: null, t), cancellationToken); - } - - /// - public async Task StoreInstanceFramesRangeAsync( - long version, - IReadOnlyDictionary framesRange, - CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(framesRange, nameof(framesRange)); - - BlockBlobClient blobClient = GetInstanceFramesRangeBlobClient(version); - - try - { - // TOOD: Stream directly to blob storage - await using RecyclableMemoryStream stream = _recyclableMemoryStreamManager.GetStream(tag: nameof(StoreInstanceFramesRangeAsync)); - await JsonSerializer.SerializeAsync(stream, framesRange, _jsonSerializerOptions, cancellationToken); - - stream.Seek(0, SeekOrigin.Begin); - await blobClient.UploadAsync( - stream, - new BlobHttpHeaders { ContentType = KnownContentTypes.ApplicationJsonUtf8 }, - metadata: null, - conditions: null, - accessTier: null, - progressHandler: null, - cancellationToken); - } - catch (Exception ex) - { - throw new DataStoreException(ex); - } - - } - - /// - public async Task> GetInstanceFramesRangeAsync(long version, CancellationToken cancellationToken) - { - BlockBlobClient cloudBlockBlob = GetInstanceFramesRangeBlobClient(version); - - try - { - return await ExecuteAsync(async t => - { - BlobDownloadResult result = await cloudBlockBlob.DownloadContentAsync(cancellationToken); - return result.Content.ToObjectFromJson>(_jsonSerializerOptions); - }, cancellationToken); - } - catch (ItemNotFoundException) - { - // With recent regression, there is a space in the blob file name, so falling back to the blob with file name if the original - // file was not found. - cloudBlockBlob = GetInstanceFramesRangeBlobClient(version, fallBackClient: true); - return await ExecuteAsync(async t => - { - BlobDownloadResult result = await cloudBlockBlob.DownloadContentAsync(cancellationToken); - - _logger.LogInformation("Successfully downloaded frame range metadata using fallback logic."); - - return result.Content.ToObjectFromJson>(_jsonSerializerOptions); - }, cancellationToken); - } - } - - /// - public async Task DoesFrameRangeExistAsync(long version, CancellationToken cancellationToken) - { - BlockBlobClient blobClient = GetInstanceFramesRangeBlobClient(version); - - return await ExecuteAsync(async t => - { - Response response = await blobClient.ExistsAsync(cancellationToken); - return response.Value; - }, cancellationToken); - } - - private BlockBlobClient GetInstanceFramesRangeBlobClient(long version, bool fallBackClient = false) - { - var blobName = fallBackClient ? _nameWithPrefix.GetInstanceFramesRangeFileNameWithSpace(version) : _nameWithPrefix.GetInstanceFramesRangeFileName(version); - return _container.GetBlockBlobClient(blobName); - } - - private BlockBlobClient GetInstanceBlockBlobClient(long version) - { - string blobName = _nameWithPrefix.GetMetadataFileName(version); - - return _container.GetBlockBlobClient(blobName); - } - - private static async Task ExecuteAsync(Func> action, CancellationToken cancellationToken) - { - try - { - return await action(cancellationToken); - } - catch (RequestFailedException ex) when (ex.ErrorCode == BlobErrorCode.BlobNotFound) - { - throw new ItemNotFoundException(ex); - } - catch (Exception ex) - { - throw new DataStoreException(ex); - } - } -} diff --git a/src/Microsoft.Health.Dicom.Blob/Features/Storage/BlobWorkitemStore.cs b/src/Microsoft.Health.Dicom.Blob/Features/Storage/BlobWorkitemStore.cs deleted file mode 100644 index 5143638429..0000000000 --- a/src/Microsoft.Health.Dicom.Blob/Features/Storage/BlobWorkitemStore.cs +++ /dev/null @@ -1,155 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.IO; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Azure; -using Azure.Storage.Blobs; -using Azure.Storage.Blobs.Models; -using Azure.Storage.Blobs.Specialized; -using EnsureThat; -using FellowOakDicom; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Health.Blob.Configs; -using Microsoft.Health.Dicom.Blob.Utilities; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Workitem; -using Microsoft.Health.Dicom.Core.Web; -using Microsoft.IO; - -namespace Microsoft.Health.Dicom.Blob.Features.Storage; - -/// -/// Provides functionality for managing the DICOM workitem instance. -/// -public class BlobWorkitemStore : IWorkitemStore -{ - private readonly BlobContainerClient _container; - private readonly JsonSerializerOptions _jsonSerializerOptions; - private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager; - private readonly ILogger _logger; - - public const int MaxPrefixLength = 3; - - public BlobWorkitemStore( - BlobServiceClient client, - IOptionsMonitor namedBlobContainerConfigurationAccessor, - RecyclableMemoryStreamManager recyclableMemoryStreamManager, - IOptions jsonSerializerOptions, - ILogger logger) - { - EnsureArg.IsNotNull(client, nameof(client)); - EnsureArg.IsNotNull(namedBlobContainerConfigurationAccessor, nameof(namedBlobContainerConfigurationAccessor)); - _jsonSerializerOptions = EnsureArg.IsNotNull(jsonSerializerOptions?.Value, nameof(jsonSerializerOptions)); - _recyclableMemoryStreamManager = EnsureArg.IsNotNull(recyclableMemoryStreamManager, nameof(recyclableMemoryStreamManager)); - _logger = EnsureArg.IsNotNull(logger, nameof(logger)); - - var containerConfiguration = namedBlobContainerConfigurationAccessor - .Get(BlobConstants.WorkitemContainerConfigurationName); - - _container = client.GetBlobContainerClient(containerConfiguration.ContainerName); - } - - /// - public async Task AddWorkitemAsync( - WorkitemInstanceIdentifier identifier, - DicomDataset dataset, - long? proposedWatermark = default, - CancellationToken cancellationToken = default) - { - EnsureArg.IsNotNull(identifier, nameof(identifier)); - EnsureArg.IsNotNull(dataset, nameof(dataset)); - - var blob = GetBlockBlobClient(identifier, proposedWatermark); - - try - { - await using RecyclableMemoryStream stream = _recyclableMemoryStreamManager.GetStream(tag: nameof(AddWorkitemAsync)); - await JsonSerializer.SerializeAsync(stream, dataset, _jsonSerializerOptions, cancellationToken); - - // Uploads the blob. Overwrites the blob if it exists, otherwise creates a new one. - stream.Seek(0, SeekOrigin.Begin); - await blob.UploadAsync( - stream, - new BlobHttpHeaders { ContentType = KnownContentTypes.ApplicationJsonUtf8 }, - metadata: null, - conditions: null, - accessTier: null, - progressHandler: null, - cancellationToken); - } - catch (Exception ex) - { - throw new DataStoreException(ex); - } - } - - /// - public async Task GetWorkitemAsync(WorkitemInstanceIdentifier identifier, CancellationToken cancellationToken = default) - { - EnsureArg.IsNotNull(identifier, nameof(identifier)); - - BlockBlobClient blobClient = GetBlockBlobClient(identifier); - - try - { - BlobDownloadResult result = await blobClient.DownloadContentAsync(cancellationToken); - return await JsonSerializer.DeserializeAsync(result.Content.ToStream(), _jsonSerializerOptions, cancellationToken); - } - catch (RequestFailedException ex) when (ex.ErrorCode == BlobErrorCode.BlobNotFound) - { - throw new ItemNotFoundException(ex); - } - catch (Exception ex) - { - throw new DataStoreException(ex); - } - } - - public async Task DeleteWorkitemAsync(WorkitemInstanceIdentifier identifier, long? proposedWatermark = default, CancellationToken cancellationToken = default) - { - EnsureArg.IsNotNull(identifier, nameof(identifier)); - - var blob = GetBlockBlobClient(identifier, proposedWatermark); - - try - { - await blob.DeleteIfExistsAsync(DeleteSnapshotsOption.None, null, cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) - { - throw new DataStoreException(ex); - } - } - - private BlockBlobClient GetBlockBlobClient(WorkitemInstanceIdentifier identifier, long? proposedWatermark = default) - { - var version = proposedWatermark.GetValueOrDefault(identifier.Watermark); - - var blobName = $"{HashingHelper.ComputeXXHash(version, MaxPrefixLength)}_{version}_workitem.json"; - - return _container.GetBlockBlobClient(blobName); - } - - private static async Task ExecuteAsync(Func> action, CancellationToken cancellationToken) - { - try - { - return await action(cancellationToken); - } - catch (RequestFailedException ex) when (ex.ErrorCode == BlobErrorCode.BlobNotFound) - { - throw new ItemNotFoundException(ex); - } - catch (Exception ex) - { - throw new DataStoreException(ex); - } - } -} diff --git a/src/Microsoft.Health.Dicom.Blob/Features/Storage/DicomFileNameWithPrefix.cs b/src/Microsoft.Health.Dicom.Blob/Features/Storage/DicomFileNameWithPrefix.cs deleted file mode 100644 index 4ac26720ac..0000000000 --- a/src/Microsoft.Health.Dicom.Blob/Features/Storage/DicomFileNameWithPrefix.cs +++ /dev/null @@ -1,39 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Microsoft.Health.Dicom.Blob.Utilities; - -namespace Microsoft.Health.Dicom.Blob.Features.Storage; - -public class DicomFileNameWithPrefix : IDicomFileNameBuilder -{ - public const int MaxPrefixLength = 3; - - public string GetInstanceFileName(long version) - { - return $"{HashingHelper.ComputeXXHash(version, MaxPrefixLength)}_{version}.dcm"; - } - - public string GetMetadataFileName(long version) - { - return $"{HashingHelper.ComputeXXHash(version, MaxPrefixLength)}_{version}_metadata.json"; - } - - public string GetInstanceFramesRangeFileName(long version) - { - return $"{HashingHelper.ComputeXXHash(version, MaxPrefixLength)}_{version}_frames_range.json"; - } - - /// - /// This method is used for the fallback logic to get the blob file with space in between - /// that was introduced in a recent regression. - /// - /// - /// - public string GetInstanceFramesRangeFileNameWithSpace(long version) - { - return $"{HashingHelper.ComputeXXHash(version, MaxPrefixLength)}_ {version}_frames_range.json"; - } -} diff --git a/src/Microsoft.Health.Dicom.Blob/Features/Storage/IBlobClient.cs b/src/Microsoft.Health.Dicom.Blob/Features/Storage/IBlobClient.cs deleted file mode 100644 index a921c104d9..0000000000 --- a/src/Microsoft.Health.Dicom.Blob/Features/Storage/IBlobClient.cs +++ /dev/null @@ -1,35 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Azure.Storage.Blobs; -using Azure.Storage.Blobs.Models; -using Microsoft.Health.Dicom.Core.Features.Common; - -namespace Microsoft.Health.Dicom.Blob.Features.Storage; - -/// -/// Responsible to get the right BlobContainerClient based on the configuration -/// -public interface IBlobClient -{ - // Making this a property to make the connection failures a API response failure instead of app/host initialization failure - BlobContainerClient BlobContainerClient { get; } - - // To support SxS behavior of current internal store and tomorrows BYOS - bool IsExternal { get; } - - /// - /// Get the service store path for the blob client as configured at startup. - /// - /// Name of the partition - string GetServiceStorePath(string partitionName); - - /// - /// Get conditions to apply to operation on blob. - /// - /// Properties of blob to use to generate conditions such as etag matching - /// BlobRequestConditions to match on eTag - BlobRequestConditions GetConditions(FileProperties fileProperties); -} diff --git a/src/Microsoft.Health.Dicom.Blob/Features/Storage/IDicomFileNameBuilder.cs b/src/Microsoft.Health.Dicom.Blob/Features/Storage/IDicomFileNameBuilder.cs deleted file mode 100644 index 9ecb08e39c..0000000000 --- a/src/Microsoft.Health.Dicom.Blob/Features/Storage/IDicomFileNameBuilder.cs +++ /dev/null @@ -1,16 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Blob.Features.Storage; -public interface IDicomFileNameBuilder -{ - string GetInstanceFileName(long version); - - string GetMetadataFileName(long version); - - string GetInstanceFramesRangeFileName(long version); - - string GetInstanceFramesRangeFileNameWithSpace(long version); -} diff --git a/src/Microsoft.Health.Dicom.Blob/Features/Storage/InternalBlobClient.cs b/src/Microsoft.Health.Dicom.Blob/Features/Storage/InternalBlobClient.cs deleted file mode 100644 index fa937e036b..0000000000 --- a/src/Microsoft.Health.Dicom.Blob/Features/Storage/InternalBlobClient.cs +++ /dev/null @@ -1,44 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Azure.Storage.Blobs; -using Azure.Storage.Blobs.Models; -using EnsureThat; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Health.Blob.Configs; -using Microsoft.Health.Dicom.Core.Features.Common; - -namespace Microsoft.Health.Dicom.Blob.Features.Storage; - -/// -/// Represents the blob container created by the service and initialized during app startup -/// -internal class InternalBlobClient : IBlobClient -{ - private readonly BlobServiceClient _client; - private readonly string _containerName; - private readonly ILogger _logger; - - public InternalBlobClient( - BlobServiceClient blobServiceClient, - IOptionsMonitor optionsMonitor, - ILogger logger) - { - _client = EnsureArg.IsNotNull(blobServiceClient, nameof(blobServiceClient)); - _containerName = EnsureArg.IsNotNull(optionsMonitor.Get(BlobConstants.BlobContainerConfigurationName).ContainerName, nameof(optionsMonitor)); - _logger = EnsureArg.IsNotNull(logger, nameof(logger)); - _logger.LogInformation("Internal blob client registered."); - } - - public bool IsExternal => false; - - public BlobContainerClient BlobContainerClient => _client.GetBlobContainerClient(_containerName); - - public string GetServiceStorePath(string partitionName) - => string.Empty; - - public BlobRequestConditions GetConditions(FileProperties fileProperties) => null; -} diff --git a/src/Microsoft.Health.Dicom.Blob/Features/Telemetry/BlobFileStoreMeter.cs b/src/Microsoft.Health.Dicom.Blob/Features/Telemetry/BlobFileStoreMeter.cs deleted file mode 100644 index 4dd004d041..0000000000 --- a/src/Microsoft.Health.Dicom.Blob/Features/Telemetry/BlobFileStoreMeter.cs +++ /dev/null @@ -1,57 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Diagnostics.Metrics; -using Microsoft.Health.Dicom.Core.Features.Telemetry; - -namespace Microsoft.Health.Dicom.Blob.Features.Telemetry; - -public sealed class BlobFileStoreMeter : IDisposable -{ - private readonly Meter _meter; - - public BlobFileStoreMeter() - { - _meter = new Meter($"{OpenTelemetryLabels.BaseMeterName}.BlobFileStore", "1.0"); - - BlobFileStoreOperationCount = - _meter.CreateCounter(nameof(BlobFileStoreOperationCount), - description: "A blob file store operation was hit."); - - BlobFileStoreOperationStreamSize = - _meter.CreateCounter(nameof(BlobFileStoreOperationStreamSize), - description: "The stream size being processed fo I/O."); - } - - /// - /// Represents a call to the blob file store operation - /// - public Counter BlobFileStoreOperationCount { get; } - - /// - /// streaming size given an operation - /// - public Counter BlobFileStoreOperationStreamSize { get; } - - /// - /// Sets telemetry dimensions on meter - /// - /// Name of operation being hit - /// Represents whether operation is input (write) or output(read) - /// Whether or not this metric is being emitted for an external store - /// - public static KeyValuePair[] CreateTelemetryDimension(string operationName, OperationType operationType, bool isExternal) => - new[] - { - new KeyValuePair("Operation", operationName), - new KeyValuePair("Type", operationType), - new KeyValuePair("IsExternal", isExternal), - }; - - public void Dispose() - => _meter.Dispose(); -} \ No newline at end of file diff --git a/src/Microsoft.Health.Dicom.Blob/Features/Telemetry/BlobRetrieveMeter.cs b/src/Microsoft.Health.Dicom.Blob/Features/Telemetry/BlobRetrieveMeter.cs deleted file mode 100644 index 7e3b322ece..0000000000 --- a/src/Microsoft.Health.Dicom.Blob/Features/Telemetry/BlobRetrieveMeter.cs +++ /dev/null @@ -1,26 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Diagnostics.Metrics; -using Microsoft.Health.Dicom.Core.Features.Telemetry; - -namespace Microsoft.Health.Dicom.Blob.Features.Telemetry; - -public sealed class BlobRetrieveMeter : IDisposable -{ - private readonly Meter _meter; - - public BlobRetrieveMeter() - { - _meter = new Meter($"{OpenTelemetryLabels.BaseMeterName}.Retrieve.Blob", "1.0"); - JsonDeserializationException = _meter.CreateCounter(nameof(JsonDeserializationException), description: "Json deserialization exception"); - } - - public Counter JsonDeserializationException { get; } - - public void Dispose() - => _meter.Dispose(); -} diff --git a/src/Microsoft.Health.Dicom.Blob/Features/Telemetry/BlobStoreMeter.cs b/src/Microsoft.Health.Dicom.Blob/Features/Telemetry/BlobStoreMeter.cs deleted file mode 100644 index a0c531e9d0..0000000000 --- a/src/Microsoft.Health.Dicom.Blob/Features/Telemetry/BlobStoreMeter.cs +++ /dev/null @@ -1,26 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Diagnostics.Metrics; -using Microsoft.Health.Dicom.Core.Features.Telemetry; - -namespace Microsoft.Health.Dicom.Blob.Features.Telemetry; - -public sealed class BlobStoreMeter : IDisposable -{ - private readonly Meter _meter; - - public BlobStoreMeter() - { - _meter = new Meter($"{OpenTelemetryLabels.BaseMeterName}.Store.Blob", "1.0"); - JsonSerializationException = _meter.CreateCounter(nameof(JsonSerializationException), description: "Json serialization exception"); - } - - public Counter JsonSerializationException { get; } - - public void Dispose() - => _meter.Dispose(); -} diff --git a/src/Microsoft.Health.Dicom.Blob/Features/Telemetry/OperationType.cs b/src/Microsoft.Health.Dicom.Blob/Features/Telemetry/OperationType.cs deleted file mode 100644 index 6513c2b022..0000000000 --- a/src/Microsoft.Health.Dicom.Blob/Features/Telemetry/OperationType.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Blob.Features.Telemetry; - -/// -/// Represents whether operation is input (write) or output(read) from perspective of I/O on the blob store -/// -public enum OperationType -{ - /// - /// For Operations that write data - /// - Input, - - /// - /// For Operations that read data - /// - Output -} \ No newline at end of file diff --git a/src/Microsoft.Health.Dicom.Blob/Microsoft.Health.Dicom.Blob.csproj b/src/Microsoft.Health.Dicom.Blob/Microsoft.Health.Dicom.Blob.csproj deleted file mode 100644 index 631bb65783..0000000000 --- a/src/Microsoft.Health.Dicom.Blob/Microsoft.Health.Dicom.Blob.csproj +++ /dev/null @@ -1,51 +0,0 @@ - - - - Azure blob storage utilities for Microsoft's DICOMweb APIs. - $(LibraryFrameworks) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - True - True - DicomBlobResource.resx - - - - - - ResXFileCodeGenerator - DicomBlobResource.Designer.cs - - - - diff --git a/src/Microsoft.Health.Dicom.Blob/Properties/AssemblyInfo.cs b/src/Microsoft.Health.Dicom.Blob/Properties/AssemblyInfo.cs deleted file mode 100644 index b5dbb56aaf..0000000000 --- a/src/Microsoft.Health.Dicom.Blob/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,15 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Resources; -using System.Runtime.CompilerServices; - -// necessary to sub iOptions -[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] -[assembly: InternalsVisibleTo("Microsoft.Health.Dicom.Blob.UnitTests")] -[assembly: InternalsVisibleTo("Microsoft.Health.Dicom.Tests.Integration")] -[assembly: NeutralResourcesLanguage("en-us")] -[assembly: CLSCompliant(false)] diff --git a/src/Microsoft.Health.Dicom.Blob/Registration/DicomBlobRegistrations.cs b/src/Microsoft.Health.Dicom.Blob/Registration/DicomBlobRegistrations.cs deleted file mode 100644 index ee22e68a22..0000000000 --- a/src/Microsoft.Health.Dicom.Blob/Registration/DicomBlobRegistrations.cs +++ /dev/null @@ -1,40 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using EnsureThat; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Blob; -using Microsoft.Health.Dicom.Blob.Features.Export; -using Microsoft.Health.Dicom.Core.Features.Export; - -namespace Microsoft.Extensions.DependencyInjection; - -internal static class DicomBlobRegistrations -{ - public static IServiceCollection AddAzureBlobExportSink( - this IServiceCollection services, - Action configureProvider = null, - Action configureClient = null) - { - EnsureArg.IsNotNull(services, nameof(services)); - - services.TryAddEnumerable(ServiceDescriptor.Scoped()); - - if (configureProvider != null) - { - services.Configure(configureProvider); - } - - OptionsBuilder builder = services.AddOptions(AzureBlobExportSinkProvider.ClientOptionsName); - if (configureClient != null) - { - builder.Configure(configureClient); - } - - return services; - } -} diff --git a/src/Microsoft.Health.Dicom.Blob/Registration/DicomFunctionBuilderMetadataRegistrationExtensions.cs b/src/Microsoft.Health.Dicom.Blob/Registration/DicomFunctionBuilderMetadataRegistrationExtensions.cs deleted file mode 100644 index 23df123d4e..0000000000 --- a/src/Microsoft.Health.Dicom.Blob/Registration/DicomFunctionBuilderMetadataRegistrationExtensions.cs +++ /dev/null @@ -1,107 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using Microsoft.Extensions.Configuration; -using Microsoft.Health.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Microsoft.Health.Blob.Configs; -using Microsoft.Health.Dicom.Blob; -using Microsoft.Health.Dicom.Blob.Features.Export; -using Microsoft.Health.Dicom.Blob.Features.ExternalStore; -using Microsoft.Health.Dicom.Blob.Features.Storage; -using Microsoft.Health.Dicom.Blob.Features.Telemetry; -using Microsoft.Health.Dicom.Blob.Utilities; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Registration; - -namespace Microsoft.Extensions.DependencyInjection; - -/// -/// A collection of extension methods for configuring blob services for Azure Functions. -/// -public static class DicomFunctionsBuilderRegistrationExtensions -{ - /// - /// Adds the metadata store for the DICOM functions. - /// - /// The DICOM functions builder instance. - /// The host configuration for the functions. - /// The name of the configuration section containing the functions. - /// The functions builder. - public static IDicomFunctionsBuilder AddBlobStorage( - this IDicomFunctionsBuilder functionsBuilder, - IConfiguration configuration, - string functionSectionName) - { - EnsureArg.IsNotNull(functionsBuilder, nameof(functionsBuilder)); - EnsureArg.IsNotNull(configuration, nameof(configuration)); - EnsureArg.IsNotNullOrWhiteSpace(functionSectionName, nameof(functionSectionName)); - - // Common services - IConfigurationSection blobConfig = configuration.GetSection(BlobServiceClientOptions.DefaultSectionName); - functionsBuilder.Services - .AddBlobServiceClient(blobConfig) - .AddOptions() - .Bind(blobConfig.GetSection(DicomBlobContainerOptions.SectionName)) - .ValidateDataAnnotations(); - - // Metadata - functionsBuilder.Services - .AddSingleton() - .AddTransient(sp => sp.GetRequiredService()) - .AddPersistence() - .AddScoped() - .AddOptions(BlobConstants.MetadataContainerConfigurationName) - .Configure>((c, o) => c.ContainerName = o.CurrentValue.Metadata); - - // Blob Files - FeatureConfiguration featureConfiguration = new FeatureConfiguration(); - configuration.GetSection("DicomServer").GetSection("Features").Bind(featureConfiguration); - if (featureConfiguration.EnableExternalStore) - { - functionsBuilder.Services.AddOptions() - .Bind(configuration.GetSection(ExternalBlobDataStoreConfiguration.SectionName)) - .ValidateDataAnnotations(); - - functionsBuilder.Services.Add() - .Singleton() - .AsSelf() - .AsImplementedInterfaces(); - - functionsBuilder.Services - .AddPersistence(); - } - else - { - functionsBuilder.Services.Add() - .Singleton() - .AsSelf() - .AsImplementedInterfaces(); - - functionsBuilder.Services - .AddSingleton() - .AddTransient(sp => sp.GetRequiredService()) - .AddPersistence() - .AddOptions(BlobConstants.BlobContainerConfigurationName) - .Configure>((c, o) => c.ContainerName = o.CurrentValue.File); - } - - // Export - functionsBuilder.Services - .AddAzureBlobExportSink( - o => configuration.GetSection(functionSectionName).GetSection(AzureBlobExportSinkProviderOptions.DefaultSection).Bind(o), - o => blobConfig.Bind(o)); // Re-use the blob store's configuration - - // Telemetry - functionsBuilder.Services - .AddSingleton() - .AddSingleton() - .AddSingleton(); - - return functionsBuilder; - } -} diff --git a/src/Microsoft.Health.Dicom.Blob/Registration/DicomServerBuilderBlobRegistrationExtensions.cs b/src/Microsoft.Health.Dicom.Blob/Registration/DicomServerBuilderBlobRegistrationExtensions.cs deleted file mode 100644 index cba547d0e1..0000000000 --- a/src/Microsoft.Health.Dicom.Blob/Registration/DicomServerBuilderBlobRegistrationExtensions.cs +++ /dev/null @@ -1,146 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using Microsoft.Extensions.Configuration; -using Microsoft.Health.Blob.Configs; -using Microsoft.Health.Blob.Features.Health; -using Microsoft.Health.Dicom.Blob.Features.Export; -using Microsoft.Health.Dicom.Blob.Features.Health; -using Microsoft.Health.Dicom.Blob.Features.Storage; -using Microsoft.Health.Dicom.Blob.Features.Telemetry; -using Microsoft.Health.Dicom.Blob.Utilities; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Blob.Features.ExternalStore; -using Microsoft.Health.Dicom.Core.Features.Workitem; -using Microsoft.Health.Dicom.Core.Registration; -using Microsoft.Health.Extensions.DependencyInjection; - -namespace Microsoft.Extensions.DependencyInjection; - -public static class DicomServerBuilderBlobRegistrationExtensions -{ - /// - /// Adds the blob data stores for the DICOM server. - /// - /// The DICOM server builder instance. - /// The configuration for the server. - /// The server builder. - public static IDicomServerBuilder AddBlobDataStores(this IDicomServerBuilder serverBuilder, IConfiguration configuration) - { - EnsureArg.IsNotNull(serverBuilder, nameof(serverBuilder)); - EnsureArg.IsNotNull(configuration, nameof(configuration)); - - IConfigurationSection blobConfig = configuration.GetSection(BlobServiceClientOptions.DefaultSectionName); - serverBuilder.Services - .AddOptions() - .Bind(blobConfig.GetSection(nameof(BlobServiceClientOptions.Operations))); - - FeatureConfiguration featureConfiguration = new FeatureConfiguration(); - configuration.GetSection("DicomServer").GetSection("Features").Bind(featureConfiguration); - if (featureConfiguration.EnableExternalStore) - { - serverBuilder.Services - .AddOptions() - .Bind(configuration.GetSection(ExternalBlobDataStoreConfiguration.SectionName)) - .ValidateDataAnnotations(); - - serverBuilder.Services.Add() - .Singleton() - .AsSelf() - .AsImplementedInterfaces(); - - serverBuilder.Services - .AddPersistence(); - - serverBuilder.Services - .AddHealthChecks() - .AddCheck("DcmHealthCheck"); - } - else - { - serverBuilder.Services.Add() - .Singleton() - .AsSelf() - .AsImplementedInterfaces(); - - serverBuilder - .AddStorageDataStore( - configuration, - "DcmHealthCheck"); - } - - serverBuilder - .AddStorageDataStore( - configuration, - "MetadataHealthCheck") - .AddStorageDataStore( - configuration, - "WorkitemHealthCheck"); - - serverBuilder.Services - .AddAzureBlobExportSink( - o => configuration.GetSection(AzureBlobExportSinkProviderOptions.DefaultSection).Bind(o), - o => blobConfig.Bind(o)); // Re-use the blob store's configuration for the client - - serverBuilder.Services - .AddSingleton() - .AddSingleton() - .AddSingleton(); - - - return serverBuilder; - } - - private static IDicomServerBuilder AddStorageDataStore( - this IDicomServerBuilder serverBuilder, IConfiguration configuration, string healthCheckName) - where TStoreConfigurationSection : class, IStoreConfigurationSection, new() - where TStore : class, TIStore - { - var blobConfig = configuration.GetSection(BlobServiceClientOptions.DefaultSectionName); - - var config = new TStoreConfigurationSection(); - - serverBuilder.Services - .AddSingleton() - .AddTransient(sp => sp.GetRequiredService()) - .AddPersistence() - .AddBlobServiceClient(blobConfig) - .AddScoped() - .AddBlobContainerInitialization(x => blobConfig - .GetSection(BlobInitializerOptions.DefaultSectionName) - .Bind(x)) - .ConfigureContainer(config.ContainerConfigurationName, x => configuration - .GetSection(config.ConfigurationSectionName) - .Bind(x)); - - serverBuilder - .AddBlobHealthCheck>(healthCheckName); - - return serverBuilder; - } - - internal static IServiceCollection AddPersistence(this IServiceCollection services) - where TStore : class, TIStore - { - services.Add() - .Scoped() - .AsSelf() - .AsImplementedInterfaces(); - - return services; - } - - internal static IDicomServerBuilder AddBlobHealthCheck(this IDicomServerBuilder serverBuilder, string name) - where TBlobHealthCheck : BlobHealthCheck - { - serverBuilder.Services - .AddHealthChecks() - .AddCheck(name: name); - - return serverBuilder; - } -} diff --git a/src/Microsoft.Health.Dicom.Blob/Utilities/ExternalBlobDataStoreConfiguration.cs b/src/Microsoft.Health.Dicom.Blob/Utilities/ExternalBlobDataStoreConfiguration.cs deleted file mode 100644 index f2ebbee7fc..0000000000 --- a/src/Microsoft.Health.Dicom.Blob/Utilities/ExternalBlobDataStoreConfiguration.cs +++ /dev/null @@ -1,39 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.ComponentModel.DataAnnotations; - -namespace Microsoft.Health.Dicom.Blob.Utilities; - -internal class ExternalBlobDataStoreConfiguration -{ - public const string SectionName = "ExternalBlobStore"; - - public Uri BlobContainerUri { get; set; } - - // use for local testing with Azurite - public string ConnectionString { get; set; } - - // use for local testing with Azurite - public string ContainerName { get; set; } - - public string HealthCheckFilePath { get; set; } = "healthCheck/health"; - - [Range(typeof(TimeSpan), "00:01:00", "1.00:00:00", ConvertValueInInvariantCulture = true, ParseLimitsInInvariantCulture = true)] - public TimeSpan HealthCheckFileExpiry { get; set; } = TimeSpan.FromMinutes(5); - - /// - /// A path which is used to store blobs along a specific path in a container, serving as a prefix to the - /// full blob name and providing a logical hierarchy when segments used though use of forward slashes (/). - /// DICOM allows any alphanumeric characters, dashes(-). periods(.) and forward slashes (/) in the service store path. - /// This path will be supplied externally when DICOM is a managed service. - /// Max of 1024 characters total and max of 254 forward slashes allowed. - /// See https://learn.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata#blob-names - /// - [RegularExpression(@"^[a-zA-Z0-9\-\.]*(\/[a-zA-Z0-9\-\.]*){0,254}$")] - [StringLength(1024)] - public string StorageDirectory { get; set; } -} diff --git a/src/Microsoft.Health.Dicom.Blob/Utilities/HashingHelper.cs b/src/Microsoft.Health.Dicom.Blob/Utilities/HashingHelper.cs deleted file mode 100644 index 81e8d5d243..0000000000 --- a/src/Microsoft.Health.Dicom.Blob/Utilities/HashingHelper.cs +++ /dev/null @@ -1,31 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Globalization; -using System.Text; -using EnsureThat; -using HashDepot; - -namespace Microsoft.Health.Dicom.Blob.Utilities; -internal static class HashingHelper -{ - /// - /// Gets a deterministic hash for a given value - /// - /// Value to be hashed - /// Length of the hash to be returned. -1 means the complete hash value without trimming. - /// Returns hashed value as string. If hashLength is provided, it will return the value to that length - public static string ComputeXXHash(long value, int hashLength = -1) - { - EnsureArg.IsNotDefault(value, nameof(value)); - - byte[] buffer = Encoding.UTF8.GetBytes(value.ToString(CultureInfo.InvariantCulture)); - var hash = XXHash.Hash64(buffer).ToString(CultureInfo.InvariantCulture); - - // If the hashLength is greater than the hash, assigning the length of the hash and not throw error - hashLength = hashLength > hash.Length ? hash.Length : hashLength; - return hash.Substring(0, hashLength == -1 ? hash.Length : hashLength); - } -} diff --git a/src/Microsoft.Health.Dicom.Blob/Utilities/IStoreConfigurationSection.cs b/src/Microsoft.Health.Dicom.Blob/Utilities/IStoreConfigurationSection.cs deleted file mode 100644 index 6565f627d3..0000000000 --- a/src/Microsoft.Health.Dicom.Blob/Utilities/IStoreConfigurationSection.cs +++ /dev/null @@ -1,16 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Blob.Utilities; - -/// -/// -/// -public interface IStoreConfigurationSection -{ - string ConfigurationSectionName { get; } - - string ContainerConfigurationName { get; } -} diff --git a/src/Microsoft.Health.Dicom.Blob/Utilities/StoreConfigurationSection.cs b/src/Microsoft.Health.Dicom.Blob/Utilities/StoreConfigurationSection.cs deleted file mode 100644 index ed93ac7189..0000000000 --- a/src/Microsoft.Health.Dicom.Blob/Utilities/StoreConfigurationSection.cs +++ /dev/null @@ -1,55 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Blob.Utilities; - -/// -/// -/// -internal sealed class BlobStoreConfigurationSection : StoreConfigurationSection -{ - public BlobStoreConfigurationSection() - : base(BlobConstants.BlobStoreConfigurationSection, BlobConstants.BlobContainerConfigurationName) - { - } -} - -/// -/// -/// -internal sealed class MetadataStoreConfigurationSection : StoreConfigurationSection -{ - public MetadataStoreConfigurationSection() - : base(BlobConstants.MetadataStoreConfigurationSection, BlobConstants.MetadataContainerConfigurationName) - { - } -} - -/// -/// -/// -internal sealed class WorkitemStoreConfigurationSection : StoreConfigurationSection -{ - public WorkitemStoreConfigurationSection() - : base(BlobConstants.WorkitemStoreConfigurationSection, BlobConstants.WorkitemContainerConfigurationName) - { - } -} - -/// -/// -/// -internal class StoreConfigurationSection : IStoreConfigurationSection -{ - internal StoreConfigurationSection(string sectionName, string name) - { - ConfigurationSectionName = sectionName; - ContainerConfigurationName = name; - } - - public string ContainerConfigurationName { get; } - - public string ConfigurationSectionName { get; } -} diff --git a/src/Microsoft.Health.Dicom.Client/DicomWebClient.cs b/src/Microsoft.Health.Dicom.Client/DicomWebClient.cs index 266ec5c9c4..259d560b2c 100644 --- a/src/Microsoft.Health.Dicom.Client/DicomWebClient.cs +++ b/src/Microsoft.Health.Dicom.Client/DicomWebClient.cs @@ -18,7 +18,7 @@ using System.Threading.Tasks; using EnsureThat; using FellowOakDicom; -using Microsoft.Health.FellowOakDicom.Serialization; +using FellowOakDicom.Serialization; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Health.Dicom.Client.Serialization; using Microsoft.Net.Http.Headers; diff --git a/src/Microsoft.Health.Dicom.Client/Microsoft.Health.Dicom.Client.csproj b/src/Microsoft.Health.Dicom.Client/Microsoft.Health.Dicom.Client.csproj index d53acfe8b0..e8901056b0 100644 --- a/src/Microsoft.Health.Dicom.Client/Microsoft.Health.Dicom.Client.csproj +++ b/src/Microsoft.Health.Dicom.Client/Microsoft.Health.Dicom.Client.csproj @@ -2,23 +2,12 @@ Defines a RESTful client for interacting with DICOMweb APIs. - $(LibraryFrameworks);netstandard2.0 + net8.0 - - - - - - - - - - - - - - + + + @@ -34,10 +23,6 @@ - - - - True diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/AutoValidationCollection.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/AutoValidationCollection.cs deleted file mode 100644 index e2042ec5a9..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/AutoValidationCollection.cs +++ /dev/null @@ -1,13 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests; - -[CollectionDefinition("Auto-Validation Collection", DisableParallelization = true)] -public class AutoValidationCollection -{ -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Exceptions/InvalidIdentifierExceptionTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Exceptions/InvalidIdentifierExceptionTests.cs deleted file mode 100644 index 1dc75c4f7e..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Exceptions/InvalidIdentifierExceptionTests.cs +++ /dev/null @@ -1,20 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Microsoft.Health.Dicom.Core.Exceptions; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Exceptions; - -public class InvalidIdentifierExceptionTests -{ - [Fact] - public void GivenInvalidIdentifierException_WhenGetMessage_ShouldReturnExpected() - { - var name = "tagname"; - var exception = new InvalidIdentifierException(name); - Assert.Equal($"Dicom element '{name}' failed validation for VR 'UI': DICOM Identifier is invalid. Value length should not exceed the maximum length of 64 characters. Value should contain characters in '0'-'9' and '.'. Each component must start with non-zero number.", exception.Message); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Extensions/DateTimeValidTestData.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Extensions/DateTimeValidTestData.cs deleted file mode 100644 index e5445f761b..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Extensions/DateTimeValidTestData.cs +++ /dev/null @@ -1,33 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections; -using System.Collections.Generic; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Extensions; - -public class DateTimeValidTestData : IEnumerable -{ - public IEnumerator GetEnumerator() - { - yield return new object[] { "20200301010203.123+0500", 2020, 03, 01, 01, 02, 03, 123 }; - yield return new object[] { "20200301010203.123-0500", 2020, 03, 01, 01, 02, 03, 123 }; - yield return new object[] { "20200301010203-0500", 2020, 03, 01, 01, 02, 03, 0 }; - yield return new object[] { "202003010102-0500", 2020, 03, 01, 01, 02, 0, 0 }; - yield return new object[] { "2020030101-0500", 2020, 03, 01, 01, 0, 0, 0 }; - yield return new object[] { "20200301-0500", 2020, 03, 01, 0, 0, 0, 0 }; - yield return new object[] { "202003-0500", 2020, 03, 01, 00, 0, 0, 0 }; - yield return new object[] { "2020-0500", 2020, 01, 01, 00, 0, 0, 0 }; - yield return new object[] { "20200301010203.123", 2020, 03, 01, 01, 02, 03, 123 }; - yield return new object[] { "20200301010203", 2020, 03, 01, 01, 02, 03, 0 }; - yield return new object[] { "202003010102", 2020, 03, 01, 01, 02, 0, 0 }; - yield return new object[] { "2020030101", 2020, 03, 01, 01, 0, 0, 0 }; - yield return new object[] { "20200301", 2020, 03, 01, 0, 0, 0, 0 }; - yield return new object[] { "202003", 2020, 03, 01, 0, 0, 0, 0 }; - yield return new object[] { "2020", 2020, 01, 01, 0, 0, 0, 0 }; - } - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Extensions/DateTimeValidUtcTestData.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Extensions/DateTimeValidUtcTestData.cs deleted file mode 100644 index 4fc1e3b6ef..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Extensions/DateTimeValidUtcTestData.cs +++ /dev/null @@ -1,26 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections; -using System.Collections.Generic; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Extensions; - -public class DateTimeValidUtcTestData : IEnumerable -{ - public IEnumerator GetEnumerator() - { - yield return new object[] { "20200301010203.123+0500", 2020, 02, 29, 20, 02, 03, 123 }; - yield return new object[] { "20200301010203.123-0500", 2020, 03, 01, 06, 02, 03, 123 }; - yield return new object[] { "20200301010203-0500", 2020, 03, 01, 06, 02, 03, 0 }; - yield return new object[] { "202003010102-0500", 2020, 03, 01, 06, 02, 0, 0 }; - yield return new object[] { "2020030101-0500", 2020, 03, 01, 06, 0, 0, 0 }; - yield return new object[] { "20200301-0500", 2020, 03, 01, 05, 0, 0, 0 }; - yield return new object[] { "202003-0500", 2020, 03, 01, 05, 0, 0, 0 }; - yield return new object[] { "2020-0500", 2020, 01, 01, 05, 0, 0, 0 }; - } - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Extensions/DateTimeWithTimezoneOffsetFromUtcValidTestData.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Extensions/DateTimeWithTimezoneOffsetFromUtcValidTestData.cs deleted file mode 100644 index 2a8d8e15ac..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Extensions/DateTimeWithTimezoneOffsetFromUtcValidTestData.cs +++ /dev/null @@ -1,26 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections; -using System.Collections.Generic; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Extensions; - -public class DateTimeWithTimezoneOffsetFromUtcValidTestData : IEnumerable -{ - public IEnumerator GetEnumerator() - { - yield return new object[] { "20200301010203.123", "+0500", 2020, 03, 01, 06, 02, 03, 123 }; - yield return new object[] { "20200301010203.123", "-0500", 2020, 02, 29, 20, 02, 03, 123 }; - yield return new object[] { "20200301010203", "-0500", 2020, 02, 29, 20, 02, 03, 0 }; - yield return new object[] { "202003010102", "-0500", 2020, 02, 29, 20, 02, 0, 0 }; - yield return new object[] { "2020030101", "-0500", 2020, 02, 29, 20, 0, 0, 0 }; - yield return new object[] { "20200301", "-0500", 2020, 02, 29, 19, 0, 0, 0 }; - yield return new object[] { "202003", "-0500", 2020, 02, 29, 19, 0, 0, 0 }; - yield return new object[] { "2020", "-0500", 2019, 12, 31, 19, 0, 0, 0 }; - } - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Extensions/DicomDatasetExtensionsTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Extensions/DicomDatasetExtensionsTests.cs deleted file mode 100644 index ade98ed0b1..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Extensions/DicomDatasetExtensionsTests.cs +++ /dev/null @@ -1,783 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.Json; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Store; -using Microsoft.Health.Dicom.Core.Features.Workitem; -using Microsoft.Health.Dicom.Core.Features.Workitem.Model; -using Microsoft.Health.Dicom.Core.Models; -using Microsoft.Health.Dicom.Tests.Common; -using Microsoft.Health.Dicom.Tests.Common.Serialization; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Extensions; - -public class DicomDatasetExtensionsTests -{ - private readonly DicomDataset _dicomDataset = new DicomDataset().NotValidated(); - - [Fact] - public void GivenDicomTagWithMultipleValue_WhenGetFirstValueOrDefaultIsCalled_ThenShouldReturnFirstOne() - { - DicomTag tag = DicomTag.AbortReason; - DicomElement element = new DicomLongString(tag, "Value1", "Value2"); - _dicomDataset.Add(element); - Assert.Equal("Value1", _dicomDataset.GetFirstValueOrDefault(tag)); - } - - [Fact] - public void GivenDicomTagWithDifferentVR_WhenGetFirstValueOrDefaultIsCalled_ThenShouldReturnNull() - { - DicomTag tag = DicomTag.AbortReason; - DicomVR expectedVR = DicomVR.CS; - DicomElement element = new DicomLongString(tag, "Value"); - _dicomDataset.Add(element); - Assert.Null(_dicomDataset.GetFirstValueOrDefault(tag, expectedVR)); - } - - [Fact] - public void GivenDicomTagDoesNotExist_WhenGetFirstValueOrDefaultIsCalled_ThenDefaultValueShouldBeReturned() - { - Assert.Equal(default, _dicomDataset.GetFirstValueOrDefault(DicomTag.StudyInstanceUID)); - Assert.Equal(default, _dicomDataset.GetFirstValueOrDefault(DicomTag.AcquisitionDateTime)); - Assert.Equal(default, _dicomDataset.GetFirstValueOrDefault(DicomTag.WarningReason)); - } - - [Fact] - public void GivenDicomTagExists_WhenGetFirstValueOrDefaultIsCalled_ThenCorrectValueShouldBeReturned() - { - const string expectedValue = "IA"; - - _dicomDataset.Add(DicomTag.InstanceAvailability, expectedValue); - - Assert.Equal(expectedValue, _dicomDataset.GetFirstValueOrDefault(DicomTag.InstanceAvailability)); - } - - [Fact] - public void GivenNoDicomDateValue_WhenGetStringDateAsDateTimeIsCalled_ThenNullShouldBeReturned() - { - Assert.Null(_dicomDataset.GetStringDateAsDate(DicomTag.StudyDate)); - } - - [Fact] - public void GivenAValidDicomDateValue_WhenGetStringDateAsDateTimeIsCalled_ThenCorrectDateTimeShouldBeReturned() - { - _dicomDataset.Add(DicomTag.StudyDate, "20200301"); - - Assert.Equal( - new DateTime(2020, 3, 1, 0, 0, 0, 0, DateTimeKind.Local), - _dicomDataset.GetStringDateAsDate(DicomTag.StudyDate).Value); - } - - [Fact] - public void GivenAnInvalidDicomDateValue_WhenGetStringDateAsDateTimeIsCalled_ThenNullShouldBeReturned() - { - _dicomDataset.Add(DicomTag.StudyDate, "2010"); - - Assert.Null(_dicomDataset.GetStringDateAsDate(DicomTag.StudyDate)); - } - - [Fact] - public void GivenNoDicomDateTimeValue_WhenGetStringDateTimeAsLiteralAndUtcDateTimesIsCalled_ThenNullShouldBeReturned() - { - Tuple result = _dicomDataset.GetStringDateTimeAsLiteralAndUtcDateTimes(DicomTag.AcquisitionDateTime); - Assert.Null(result.Item1); - Assert.Null(result.Item2); - } - - [Theory] - [ClassData(typeof(DateTimeValidTestData))] - public void GivenAValidDicomDateTimeValue_WhenGetStringDateTimeAsLiteralAndUtcDateTimesIsCalled_ThenCorrectLiteralDateTimesShouldBeReturned( - string acquisitionDateTime, - int year, - int month, - int day, - int hour, - int minute, - int second, - int millisecond - ) - { - _dicomDataset.Add(DicomTag.AcquisitionDateTime, acquisitionDateTime); - Assert.Equal( - new DateTime( - year, - month, - day, - hour, - minute, - second, - millisecond), - _dicomDataset.GetStringDateTimeAsLiteralAndUtcDateTimes(DicomTag.AcquisitionDateTime).Item1.Value); - } - - [Theory] - [ClassData(typeof(DateTimeValidUtcTestData))] - public void GivenAValidDicomDateTimeValueWithOffset_WhenGetStringDateTimeAsLiteralAndUtcDateTimesIsCalled_ThenCorrectUtcDateTimesShouldBeReturned( - string acquisitionDateTime, - int year, - int month, - int day, - int hour, - int minute, - int second, - int millisecond - ) - { - _dicomDataset.Add(DicomTag.AcquisitionDateTime, acquisitionDateTime); - Assert.Equal( - new DateTime( - year, - month, - day, - hour, - minute, - second, - millisecond), - _dicomDataset.GetStringDateTimeAsLiteralAndUtcDateTimes(DicomTag.AcquisitionDateTime).Item2.Value); - } - - [Theory] - [ClassData(typeof(DateTimeWithTimezoneOffsetFromUtcValidTestData))] - public void GivenAValidDicomDateTimeWithoutOffsetWithTimezoneOffsetFromUtc_WhenGetStringDateTimeAsLiteralAndUtcDateTimesIsCalled_ThenCorrectUtcDateTimesShouldBeReturned( - string acquisitionDateTime, - string timezoneOffsetFromUTC, - int year, - int month, - int day, - int hour, - int minute, - int second, - int millisecond) - { - _dicomDataset.Add(DicomTag.AcquisitionDateTime, acquisitionDateTime); - _dicomDataset.Add(DicomTag.TimezoneOffsetFromUTC, timezoneOffsetFromUTC); - Assert.Equal( - new DateTime( - year, - month, - day, - hour, - minute, - second, - millisecond), - _dicomDataset.GetStringDateTimeAsLiteralAndUtcDateTimes(DicomTag.AcquisitionDateTime).Item2.Value); - } - - [Fact] - public void GivenAValidDicomDateTimeValueWithoutOffset_WhenGetStringDateTimeAsLiteralAndUtcDateTimesIsCalled_ThenNullIsReturnedForUtcDateTime() - { - _dicomDataset.Add(DicomTag.AcquisitionDateTime, "20200102030405.678"); - - Assert.Null(_dicomDataset.GetStringDateTimeAsLiteralAndUtcDateTimes(DicomTag.AcquisitionDateTime).Item2); - } - - [Theory] - [InlineData("20200301010203.123+9900")] - [InlineData("20200301010203.123-9900")] - [InlineData("20200301010203123+0500")] - [InlineData("20209901010203+0500")] - [InlineData("20200399010203+0500")] - [InlineData("20200301990203+0500")] - [InlineData("20200301019903+0500")] - [InlineData("20200301010299+0500")] - [InlineData("20200301010299.")] - [InlineData("20200301010299123")] - [InlineData("20209901010203")] - [InlineData("20200399010203")] - [InlineData("20200301990203")] - [InlineData("20200301019903")] - [InlineData("20200301010299")] - [InlineData("31")] - public void GivenAnInvalidDicomDateTimeValue_WhenGetStringDateTimeAsLiteralAndUtcDateTimesIsCalled_ThenNullShouldBeReturned(string acquisitionDateTime) - { - _dicomDataset.Add(DicomTag.AcquisitionDateTime, acquisitionDateTime); - - Assert.Null(_dicomDataset.GetStringDateTimeAsLiteralAndUtcDateTimes(DicomTag.AcquisitionDateTime).Item1); - } - - [Fact] - public void GivenNoDicomTimeValue_WhenGetStringTimeAsLongIsCalled_ThenNullShouldBeReturned() - { - Assert.Null(_dicomDataset.GetStringTimeAsLong(DicomTag.StudyTime)); - } - - [Theory] - [InlineData("010203.123", 01, 02, 03, 123)] - [InlineData("010203", 01, 02, 03, 0)] - [InlineData("0102", 01, 02, 0, 0)] - [InlineData("01", 01, 0, 0, 0)] - public void GivenAValidDicomTimeValue_WhenGetStringTimeAsLongIsCalled_ThenCorrectTimeTicksShouldBeReturned( - string studyTime, - int hour, - int minute, - int second, - int millisecond - ) - { - _dicomDataset.Add(DicomTag.StudyTime, studyTime); - Assert.Equal( - new DateTime( - 01, - 01, - 01, - hour, - minute, - second, - millisecond).Ticks, - _dicomDataset.GetStringTimeAsLong(DicomTag.StudyTime).Value); - } - - [Theory] - [InlineData("010299123")] - [InlineData("010299")] - [InlineData("019903")] - [InlineData("990203")] - [InlineData("2")] - public void GivenAnInvalidDicomTimeValue_WhenGetStringTimeAsLongIsCalled_ThenNullShouldBeReturned(string studyTime) - { - _dicomDataset.Add(DicomTag.StudyTime, studyTime); - - Assert.Null(_dicomDataset.GetStringTimeAsLong(DicomTag.StudyTime)); - } - - [Fact] - public void GivenANullValue_WhenAddValueIfNotNullIsCalled_ThenValueShouldNotBeAdded() - { - DicomTag dicomTag = DicomTag.StudyInstanceUID; - - _dicomDataset.AddValueIfNotNull(dicomTag, (string)null); - - Assert.False(_dicomDataset.TryGetSingleValue(dicomTag, out string _)); - } - - [Fact] - public void GivenANonNullValue_WhenAddValueIfNotNullIsCalled_ThenValueShouldBeAdded() - { - const string value = "123"; - - DicomTag dicomTag = DicomTag.StudyInstanceUID; - - _dicomDataset.AddValueIfNotNull(dicomTag, value); - - Assert.True(_dicomDataset.TryGetSingleValue(dicomTag, out string writtenValue)); - Assert.Equal(value, writtenValue); - } - - [Fact] - public void GivenADicomDataset_WhenCopiedWithoutBulkDataItems_ThenCorrectDicomDatasetShouldBeCreated() - { - var dicomItemsToCopy = new Dictionary(); - - AddCopy(() => new DicomApplicationEntity(DicomTag.RetrieveAETitle, "ae")); - AddCopy(() => new DicomAgeString(DicomTag.PatientAge, "011Y")); - AddCopy(() => new DicomAttributeTag(DicomTag.DimensionIndexPointer, DicomTag.DimensionIndexSequence)); - AddCopy(() => new DicomCodeString(DicomTag.Modality, "MRI")); - AddCopy(() => new DicomDate(DicomTag.StudyDate, "20200105")); - AddCopy(() => new DicomDecimalString(DicomTag.PatientSize, "153.5")); - AddCopy(() => new DicomDateTime(DicomTag.AcquisitionDateTime, new DateTime(2020, 5, 1, 13, 20, 49, 403, DateTimeKind.Utc))); - AddCopy(() => new DicomFloatingPointSingle(DicomTag.RecommendedDisplayFrameRateInFloat, 1.5f)); - AddCopy(() => new DicomFloatingPointDouble(DicomTag.EventTimeOffset, 1.50)); - AddCopy(() => new DicomIntegerString(DicomTag.StageNumber, 1)); - AddCopy(() => new DicomLongString(DicomTag.EventTimerNames, "string1", "string2")); - AddCopy(() => new DicomLongText(DicomTag.PatientComments, "comment")); - AddCopy(() => new DicomPersonName(DicomTag.PatientName, "Jon^Doe")); - AddCopy(() => new DicomShortString(DicomTag.Occupation, "IT")); - AddCopy(() => new DicomSignedLong(DicomTag.ReferencePixelX0, -123)); - AddCopy(() => new DicomSignedShort(DicomTag.TagAngleSecondAxis, -50)); - AddCopy(() => new DicomShortText(DicomTag.CodingSchemeName, "text")); - AddCopy(() => new DicomSignedVeryLong(new DicomTag(7777, 1234), -12345)); - AddCopy(() => new DicomTime(DicomTag.Time, "172230")); - AddCopy(() => new DicomUnlimitedCharacters(DicomTag.LongCodeValue, "long value")); - AddCopy(() => new DicomUniqueIdentifier(DicomTag.StudyInstanceUID, "1.2.3")); - AddCopy(() => new DicomUnsignedLong(DicomTag.SimpleFrameList, 1, 2, 3)); - AddCopy(() => new DicomUniversalResource(DicomTag.RetrieveURL, "https://localhost/")); - AddCopy(() => new DicomUnsignedShort(DicomTag.NumberOfElements, 50)); - AddCopy(() => new DicomUnlimitedText(DicomTag.StrainAdditionalInformation, "unlimited text")); - AddCopy(() => new DicomUnsignedVeryLong(new DicomTag(7777, 9865), 243)); - - var dicomItemsNotToCopy = new Dictionary(); - - AddDoNotCopy(() => new DicomOtherByte(DicomTag.BadPixelImage, new byte[] { 1, 2, 3 })); - AddDoNotCopy(() => new DicomOtherDouble(DicomTag.VolumetricCurvePoints, 12.3)); - AddDoNotCopy(() => new DicomOtherFloat(DicomTag.TwoDimensionalToThreeDimensionalMapData, 1.24f)); - AddDoNotCopy(() => new DicomOtherLong(DicomTag.LongPrimitivePointIndexList, 1)); - AddDoNotCopy(() => new DicomOtherVeryLong(DicomTag.ExtendedOffsetTable, 23242394)); - AddDoNotCopy(() => new DicomOtherWord(DicomTag.SegmentedAlphaPaletteColorLookupTableData, 213)); - AddDoNotCopy(() => new DicomUnknown(new DicomTag(7777, 1357), new byte[] { 10, 15, 20 })); - - var sequence = new DicomSequence( - DicomTag.ReferencedSOPSequence, - new DicomDataset( - dicomItemsNotToCopy[typeof(DicomOtherByte)], - dicomItemsNotToCopy[typeof(DicomOtherDouble)], - dicomItemsToCopy[typeof(DicomIntegerString)], - dicomItemsNotToCopy[typeof(DicomOtherFloat)]), - new DicomDataset( - dicomItemsNotToCopy[typeof(DicomOtherLong)], - dicomItemsToCopy[typeof(DicomPersonName)], - dicomItemsNotToCopy[typeof(DicomOtherVeryLong)], - dicomItemsNotToCopy[typeof(DicomOtherWord)], - dicomItemsToCopy[typeof(DicomShortString)], - dicomItemsNotToCopy[typeof(DicomUnknown)], - new DicomSequence( - DicomTag.FailedSOPSequence, - new DicomDataset( - dicomItemsNotToCopy[typeof(DicomUnknown)])))); - - // Create a dataset that includes all VR types. - DicomDataset dicomDataset = new DicomDataset( - dicomItemsToCopy.Values.Concat(dicomItemsNotToCopy.Values).Concat(new[] { sequence })); - - // Make a copy of the dataset without the bulk data. - DicomDataset copiedDicomDataset = dicomDataset.CopyWithoutBulkDataItems(); - - Assert.NotNull(copiedDicomDataset); - - // Make sure it's a copy. - Assert.NotSame(dicomDataset, copiedDicomDataset); - - // Make sure the original dataset was not altered. - Assert.Equal(dicomItemsToCopy.Count + dicomItemsNotToCopy.Count + 1, dicomDataset.Count()); - - // The expected number of items are dicomItemsToCopy + sequence. - Assert.Equal(dicomItemsToCopy.Count + 1, copiedDicomDataset.Count()); - - var expectedSequence = new DicomSequence( - DicomTag.ReferencedSOPSequence, - new DicomDataset( - dicomItemsToCopy[typeof(DicomIntegerString)]), - new DicomDataset( - dicomItemsToCopy[typeof(DicomPersonName)], - dicomItemsToCopy[typeof(DicomShortString)], - new DicomSequence( - DicomTag.FailedSOPSequence, - new DicomDataset()))); - - var expectedDicomDataset = new DicomDataset( - dicomItemsToCopy.Values.Concat(new[] { expectedSequence })); - - // There is no easy way to compare the DicomItem (it doesn't implement IComparable or - // have a consistent way of getting the value out of it. So we will cheat a little bit - // by serialize the DicomDataset into JSON string. The serializer ensures the items are - // ordered properly. - Assert.Equal( - JsonSerializer.Serialize(expectedDicomDataset, AppSerializerOptions.Json), - JsonSerializer.Serialize(copiedDicomDataset, AppSerializerOptions.Json)); - - void AddCopy(Func creator) - where T : DicomItem - { - dicomItemsToCopy.Add(typeof(T), creator()); - } - - void AddDoNotCopy(Func creator) - where T : DicomItem - { - dicomItemsNotToCopy.Add(typeof(T), creator()); - } - } - - [Theory] - [MemberData(nameof(ValidAttributeRequirements))] - public void GivenADataset_WhenRequirementIsMet_ValidationSucceeds(DicomTag tag, DicomItem item, RequirementCode requirement) - { - var dataset = new DicomDataset(item); - - dataset.ValidateRequirement(tag, requirement); - } - - [Theory] - [MemberData(nameof(InvalidAttributeRequirements))] - public void GivenADataset_WhenRequirementIsNotMet_ValidationFails(DicomTag tag, DicomItem item, RequirementCode requirement) - { - var dataset = new DicomDataset(item); - - Assert.Throws(() => dataset.ValidateRequirement(tag, requirement)); - } - - [Fact] - public void GivenADataset_WhenProcedureStepStateIsCanceledFinalStateRequirementRIsNotMet_ValidationFails() - { - var dataset = Samples.CreateRandomWorkitemInstanceDataset(); - dataset.Remove(DicomTag.SOPInstanceUID); - - Assert.Throws(() => - dataset.ValidateRequirement(DicomTag.SOPInstanceUID, ProcedureStepState.Canceled, FinalStateRequirementCode.R)); - } - - [Fact] - public void GivenADataset_WhenProcedureStepStateIsCanceledFinalStateRequirementRCIsNotMet_ValidationFails() - { - var dataset = Samples.CreateRandomWorkitemInstanceDataset(); - dataset.Remove(DicomTag.SOPInstanceUID); - - Assert.Throws(() => - dataset.ValidateRequirement(DicomTag.SOPInstanceUID, ProcedureStepState.Canceled, FinalStateRequirementCode.RC, (ds, t) => true)); - } - - [Fact] - public void GivenADataset_WhenProcedureStepStateIsCanceledFinalStateRequirementP_DoesNotValidate() - { - var dataset = Samples.CreateRandomWorkitemInstanceDataset(); - dataset.Remove(DicomTag.SOPInstanceUID); - - dataset.ValidateRequirement(DicomTag.SOPInstanceUID, ProcedureStepState.Canceled, FinalStateRequirementCode.P); - } - - [Fact] - public void GivenADataset_WhenProcedureStepStateIsCanceledFinalStateRequirementXIsNotMet_ValidationFails() - { - var dataset = Samples.CreateRandomWorkitemInstanceDataset(); - dataset.Remove(DicomTag.SOPInstanceUID); - - Assert.Throws(() => - dataset.ValidateRequirement(DicomTag.SOPInstanceUID, ProcedureStepState.Canceled, FinalStateRequirementCode.X)); - } - - [Fact] - public void GivenADataset_WhenProcedureStepStateIsCanceledFinalStateRequirementRIsMet_ValidationSucceeds() - { - var dataset = Samples.CreateRandomWorkitemInstanceDataset(); - - dataset.ValidateRequirement(DicomTag.SOPInstanceUID, ProcedureStepState.Canceled, FinalStateRequirementCode.R); - } - - [Fact] - public void GivenADataset_WhenProcedureStepStateIsCanceledFinalStateRequirementRCIsMet_ValidationSucceeds() - { - var dataset = Samples.CreateRandomWorkitemInstanceDataset(); - - dataset.ValidateRequirement(DicomTag.SOPInstanceUID, ProcedureStepState.Canceled, FinalStateRequirementCode.RC, (ds, t) => true); - } - - [Fact] - public void GivenADataset_WhenProcedureStepStateIsCanceledFinalStateRequirementPIsMet_ValidationSucceeds() - { - var dataset = Samples.CreateRandomWorkitemInstanceDataset(); - - dataset.ValidateRequirement(DicomTag.SOPInstanceUID, ProcedureStepState.Canceled, FinalStateRequirementCode.P); - } - - [Fact] - public void GivenADataset_WhenProcedureStepStateIsCanceledFinalStateRequirementXIsMet_ValidationSucceeds() - { - var dataset = Samples.CreateRandomWorkitemInstanceDataset(); - - dataset.ValidateRequirement(DicomTag.SOPInstanceUID, ProcedureStepState.Canceled, FinalStateRequirementCode.X); - } - - [Fact] - public void GivenADataset_WhenProcedureStepStateIsCanceledFinalStateRequirementOIsMet_ValidationSucceeds() - { - var dataset = Samples.CreateRandomWorkitemInstanceDataset(); - dataset.Remove(DicomTag.SOPInstanceUID); - - dataset.ValidateRequirement(DicomTag.SOPInstanceUID, ProcedureStepState.Canceled, FinalStateRequirementCode.O); - } - - [Fact] - public void GivenADataset_WhenProcedureStepStateIsCompletedFinalStateRequirementRIsNotMet_ValidationFails() - { - var dataset = Samples.CreateRandomWorkitemInstanceDataset(); - dataset.Remove(DicomTag.SOPInstanceUID); - - Assert.Throws(() => - dataset.ValidateRequirement(DicomTag.SOPInstanceUID, ProcedureStepState.Completed, FinalStateRequirementCode.R)); - } - - [Fact] - public void GivenADataset_WhenProcedureStepStateIsCompletedFinalStateRequirementRCIsNotMet_ValidationFails() - { - var dataset = Samples.CreateRandomWorkitemInstanceDataset(); - dataset.Remove(DicomTag.SOPInstanceUID); - - Assert.Throws(() => - dataset.ValidateRequirement(DicomTag.SOPInstanceUID, ProcedureStepState.Completed, FinalStateRequirementCode.RC, (ds, t) => true)); - } - - [Fact] - public void GivenADataset_WhenProcedureStepStateIsCompletedFinalStateRequirementPIsNotMet_ValidationFails() - { - var dataset = Samples.CreateRandomWorkitemInstanceDataset(); - dataset.Remove(DicomTag.SOPInstanceUID); - - Assert.Throws(() => - dataset.ValidateRequirement(DicomTag.SOPInstanceUID, ProcedureStepState.Completed, FinalStateRequirementCode.P)); - } - - [Fact] - public void GivenADataset_WhenProcedureStepStateIsCompletedFinalStateRequirementX_DoesNotValidate() - { - var dataset = Samples.CreateRandomWorkitemInstanceDataset(); - dataset.Remove(DicomTag.SOPInstanceUID); - - dataset.ValidateRequirement(DicomTag.SOPInstanceUID, ProcedureStepState.Completed, FinalStateRequirementCode.X); - } - - [Fact] - public void GivenADataset_WhenProcedureStepStateIsCompletedFinalStateRequirementRIsMet_ValidationSucceeds() - { - var dataset = Samples.CreateRandomWorkitemInstanceDataset(); - - dataset.ValidateRequirement(DicomTag.SOPInstanceUID, ProcedureStepState.Completed, FinalStateRequirementCode.R); - } - - [Fact] - public void GivenADataset_WhenProcedureStepStateIsCompletedFinalStateRequirementRCIsMet_ValidationSucceeds() - { - var dataset = Samples.CreateRandomWorkitemInstanceDataset(); - - dataset.ValidateRequirement(DicomTag.SOPInstanceUID, ProcedureStepState.Completed, FinalStateRequirementCode.RC, (ds, t) => true); - } - - [Fact] - public void GivenADataset_WhenProcedureStepStateIsCompletedFinalStateRequirementPIsMet_ValidationSucceeds() - { - var dataset = Samples.CreateRandomWorkitemInstanceDataset(); - - dataset.ValidateRequirement(DicomTag.SOPInstanceUID, ProcedureStepState.Completed, FinalStateRequirementCode.P); - } - - [Fact] - public void GivenADataset_WhenProcedureStepStateIsCompletedFinalStateRequirementOIsMet_ValidationSucceeds() - { - var dataset = Samples.CreateRandomWorkitemInstanceDataset(); - dataset.Remove(DicomTag.SOPInstanceUID); - - dataset.ValidateRequirement(DicomTag.SOPInstanceUID, ProcedureStepState.Completed, FinalStateRequirementCode.O); - } - - [Fact] - public void GivenAddWorkitemDataset_WhenAllRequirementsAreMet_ValidationSucceeds() - { - var dataset = Samples.CreateRandomWorkitemInstanceDataset(requestType: WorkitemRequestType.Add); - dataset.ValidateAllRequirements(WorkitemRequestType.Add); - } - - [Fact] - public void GivenUpdateWorkitemDataset_WhenAllRequirementsAreMet_ValidationSucceeds() - { - var dataset = Samples.CreateRandomWorkitemInstanceDataset(requestType: WorkitemRequestType.Update); - dataset.ValidateAllRequirements(WorkitemRequestType.Update); - } - - [Fact] - public void GivenAddWorkitemDataset_WhenRequirementIsNotMet_ValidationFails() - { - var dataset = Samples.CreateRandomWorkitemInstanceDataset(requestType: WorkitemRequestType.Update); - Assert.Throws(() => dataset.ValidateAllRequirements(WorkitemRequestType.Add)); - } - - [Fact] - public void GivenUpdateWorkitemDataset_WhenRequirementIsNotMet_ValidationFails() - { - var dataset = Samples.CreateRandomWorkitemInstanceDataset(requestType: WorkitemRequestType.Add); - Assert.Throws(() => dataset.ValidateAllRequirements(WorkitemRequestType.Update)); - } - - [Fact] - public void GivenNullDataset_WhenTryGetLargeDicomItemIsCalled_ThrowsException() - { - DicomDataset dataset = null; - DicomItem largeDicomItem; - - Assert.Throws(() => Core.Extensions.DicomDatasetExtensions.TryGetLargeDicomItem(dataset, 1, 10, out largeDicomItem)); - } - - [Fact] - public void GivenInvalidObjectSize_WhenTryGetLargeDicomItemIsCalled_ThrowsException() - { - var dataset = new DicomDataset(); - DicomItem largeDicomItem; - - Assert.Throws(() => dataset.TryGetLargeDicomItem(-1, 10, out largeDicomItem)); - Assert.Throws(() => dataset.TryGetLargeDicomItem(1, -1, out largeDicomItem)); - } - - [Fact] - public void GivenInputWithNoLargeItem_WhenTryGetLargeDicomItemIsCalled_ReturnNullLargeItem() - { - var dataset = new DicomDataset - { - { DicomTag.PatientID, "TEST" }, - { DicomTag.AccessionNumber, "12345678" } - }; - DicomItem largeDicomItem; - - var result = dataset.TryGetLargeDicomItem(1000, 10000, out largeDicomItem); - - Assert.False(result); - Assert.Null(largeDicomItem); - } - - [Fact] - public void GivenInputWithLargeItem_WhenTryGetLargeDicomItemIsCalled_ReturnLargeItem() - { - var buffer = new byte[5000]; - var dataset = new DicomDataset - { - { DicomTag.PixelData, buffer }, - { DicomTag.PatientID, "TEST" }, - }; - DicomItem largeDicomItem; - - var result = dataset.TryGetLargeDicomItem(1000, 10000, out largeDicomItem); - - Assert.True(result); - Assert.NotNull(largeDicomItem); - } - - [Fact] - public void GivenInputWithNoLargeItem_WhenTryGetLargeDicomItemIsCalled_ReturnLargeItemWhichMatchMaxLargeSize() - { - var buffer = new byte[500]; - var dataset = new DicomDataset - { - { DicomTag.PixelData, buffer }, - { DicomTag.PatientID, "TEST" }, - }; - DicomItem largeDicomItem; - - var result = dataset.TryGetLargeDicomItem(100, 501, out largeDicomItem); - - Assert.True(result); - Assert.NotNull(largeDicomItem); - Assert.Equal(DicomTag.PixelData, largeDicomItem.Tag); - } - - - [Fact] - public void TryGetLargeDicomItem_Returns_TotalSize_And_LargeDicomItem() - { - var dicomItem = new DicomUniqueIdentifier(DicomTag.SOPClassUID, "1.2.840.10008.5.1.4.1.1.7"); - var dataset = new DicomDataset - { - dicomItem, - new DicomShortString(DicomTag.SOPInstanceUID, "1.2.3.4.5"), - new DicomCodeString(DicomTag.Modality, "MR") - }; - var element = new DicomOtherWord(DicomTag.PixelData, new ushort[] { 1, 2, 3, 4 }); - dataset.Add(element); - - var minLargeObjectsizeInBytes = 8; - var maxLargeObjectsizeInBytes = 100; - DicomItem largeDicomItem; - - var result = dataset.TryGetLargeDicomItem(minLargeObjectsizeInBytes, maxLargeObjectsizeInBytes, out largeDicomItem); - - Assert.True(result); - Assert.Equal(dicomItem, largeDicomItem); - } - - public static IEnumerable ValidAttributeRequirements() - { - yield return new object[] { DicomTag.PatientBirthName, new DicomPersonName(DicomTag.PatientBirthName, "foo"), RequirementCode.OneOne }; - yield return new object[] { DicomTag.PatientBirthName, new DicomPersonName(DicomTag.PatientBirthName, "foo"), RequirementCode.TwoOne }; - yield return new object[] { DicomTag.PatientBirthName, new DicomPersonName(DicomTag.PatientBirthName, string.Empty), RequirementCode.TwoOne }; - yield return new object[] { DicomTag.PatientBirthName, new DicomPersonName(DicomTag.PatientBirthName, "foo"), RequirementCode.ThreeTwo }; - yield return new object[] { DicomTag.PatientBirthName, new DicomPersonName(DicomTag.PatientBirthName, "foo"), RequirementCode.ThreeThree }; - yield return new object[] { DicomTag.PatientBirthName, new DicomPersonName(DicomTag.PatientBirthName, "foo"), RequirementCode.OneCOne }; - yield return new object[] { DicomTag.PatientBirthName, new DicomPersonName(DicomTag.PatientBirthName, "foo"), RequirementCode.OneCOneC }; - yield return new object[] { DicomTag.PatientBirthName, new DicomPersonName(DicomTag.PatientBirthName, "foo"), RequirementCode.OneCTwo }; - yield return new object[] { DicomTag.PatientBirthName, new DicomPersonName(DicomTag.PatientBirthName, "foo"), RequirementCode.TwoCTwoC }; - yield return new object[] { DicomTag.PatientBirthName, new DicomPersonName(DicomTag.PatientBirthName, string.Empty), RequirementCode.TwoCTwoC }; - yield return new object[] { DicomTag.PatientBirthName, new DicomPersonName(DicomTag.PatientBirthName, string.Empty), RequirementCode.MustBeEmpty }; - - yield return new object[] { DicomTag.ApprovalStatusDateTime, new DicomDateTime(DicomTag.ApprovalStatusDateTime, DateTime.UtcNow), RequirementCode.OneOne }; - yield return new object[] { DicomTag.ApprovalStatusDateTime, new DicomDateTime(DicomTag.ApprovalStatusDateTime, DateTime.UtcNow), RequirementCode.TwoOne }; - yield return new object[] { DicomTag.ApprovalStatusDateTime, new DicomDateTime(DicomTag.ApprovalStatusDateTime, new string[0]), RequirementCode.TwoOne }; - yield return new object[] { DicomTag.ApprovalStatusDateTime, new DicomDateTime(DicomTag.ApprovalStatusDateTime, DateTime.UtcNow), RequirementCode.ThreeTwo }; - yield return new object[] { DicomTag.ApprovalStatusDateTime, new DicomDateTime(DicomTag.ApprovalStatusDateTime, DateTime.UtcNow), RequirementCode.ThreeThree }; - yield return new object[] { DicomTag.ApprovalStatusDateTime, new DicomDateTime(DicomTag.ApprovalStatusDateTime, DateTime.UtcNow), RequirementCode.OneCOne }; - yield return new object[] { DicomTag.ApprovalStatusDateTime, new DicomDateTime(DicomTag.ApprovalStatusDateTime, DateTime.UtcNow), RequirementCode.OneCOneC }; - yield return new object[] { DicomTag.ApprovalStatusDateTime, new DicomDateTime(DicomTag.ApprovalStatusDateTime, DateTime.UtcNow), RequirementCode.OneCTwo }; - yield return new object[] { DicomTag.ApprovalStatusDateTime, new DicomDateTime(DicomTag.ApprovalStatusDateTime, DateTime.UtcNow), RequirementCode.TwoCTwoC }; - yield return new object[] { DicomTag.ApprovalStatusDateTime, new DicomDateTime(DicomTag.ApprovalStatusDateTime, new string[0]), RequirementCode.TwoCTwoC }; - yield return new object[] { DicomTag.ApprovalStatusDateTime, new DicomDateTime(DicomTag.ApprovalStatusDateTime, new string[0]), RequirementCode.MustBeEmpty }; - - yield return new object[] { DicomTag.SelectorSLValue, new DicomSignedLong(DicomTag.SelectorSLValue, 0), RequirementCode.OneOne }; - yield return new object[] { DicomTag.SelectorSLValue, new DicomSignedLong(DicomTag.SelectorSLValue, 0), RequirementCode.TwoOne }; - yield return new object[] { DicomTag.SelectorSLValue, new DicomSignedLong(DicomTag.SelectorSLValue), RequirementCode.TwoOne }; - yield return new object[] { DicomTag.SelectorSLValue, new DicomSignedLong(DicomTag.SelectorSLValue, 0), RequirementCode.ThreeTwo }; - yield return new object[] { DicomTag.SelectorSLValue, new DicomSignedLong(DicomTag.SelectorSLValue, 0), RequirementCode.ThreeThree }; - yield return new object[] { DicomTag.SelectorSLValue, new DicomSignedLong(DicomTag.SelectorSLValue, 0), RequirementCode.OneCOne }; - yield return new object[] { DicomTag.SelectorSLValue, new DicomSignedLong(DicomTag.SelectorSLValue, 0), RequirementCode.OneCOneC }; - yield return new object[] { DicomTag.SelectorSLValue, new DicomSignedLong(DicomTag.SelectorSLValue, 0), RequirementCode.OneCTwo }; - yield return new object[] { DicomTag.SelectorSLValue, new DicomSignedLong(DicomTag.SelectorSLValue, 0), RequirementCode.TwoCTwoC }; - yield return new object[] { DicomTag.SelectorSLValue, new DicomSignedLong(DicomTag.SelectorSLValue), RequirementCode.TwoCTwoC }; - yield return new object[] { DicomTag.SelectorSLValue, new DicomSignedLong(DicomTag.SelectorSLValue), RequirementCode.MustBeEmpty }; - - yield return new object[] { DicomTag.SnoutSequence, new DicomSequence(DicomTag.SnoutSequence, new DicomDataset[] { new DicomDataset(new DicomDecimalString(DicomTag.PixelBandwidth, "1.0")) }), RequirementCode.OneOne }; - yield return new object[] { DicomTag.SnoutSequence, new DicomSequence(DicomTag.SnoutSequence, new DicomDataset[] { new DicomDataset(new DicomDecimalString(DicomTag.PixelBandwidth, "1.0")) }), RequirementCode.TwoOne }; - yield return new object[] { DicomTag.SnoutSequence, new DicomSequence(DicomTag.SnoutSequence), RequirementCode.TwoOne }; - yield return new object[] { DicomTag.SnoutSequence, new DicomSequence(DicomTag.SnoutSequence, new DicomDataset[] { new DicomDataset(new DicomDecimalString(DicomTag.PixelBandwidth, "1.0")) }), RequirementCode.ThreeTwo }; - yield return new object[] { DicomTag.SnoutSequence, new DicomSequence(DicomTag.SnoutSequence, new DicomDataset[] { new DicomDataset(new DicomDecimalString(DicomTag.PixelBandwidth, "1.0")) }), RequirementCode.ThreeThree }; - yield return new object[] { DicomTag.SnoutSequence, new DicomSequence(DicomTag.SnoutSequence, new DicomDataset[] { new DicomDataset(new DicomDecimalString(DicomTag.PixelBandwidth, "1.0")) }), RequirementCode.OneCOne }; - yield return new object[] { DicomTag.SnoutSequence, new DicomSequence(DicomTag.SnoutSequence, new DicomDataset[] { new DicomDataset(new DicomDecimalString(DicomTag.PixelBandwidth, "1.0")) }), RequirementCode.OneCOneC }; - yield return new object[] { DicomTag.SnoutSequence, new DicomSequence(DicomTag.SnoutSequence, new DicomDataset[] { new DicomDataset(new DicomDecimalString(DicomTag.PixelBandwidth, "1.0")) }), RequirementCode.OneCTwo }; - yield return new object[] { DicomTag.SnoutSequence, new DicomSequence(DicomTag.SnoutSequence, new DicomDataset[] { new DicomDataset(new DicomDecimalString(DicomTag.PixelBandwidth, "1.0")) }), RequirementCode.TwoCTwoC }; - yield return new object[] { DicomTag.SnoutSequence, new DicomSequence(DicomTag.SnoutSequence), RequirementCode.TwoCTwoC }; - yield return new object[] { DicomTag.SnoutSequence, new DicomSequence(DicomTag.SnoutSequence), RequirementCode.MustBeEmpty }; - } - - public static IEnumerable InvalidAttributeRequirements() - { - // not present - yield return new object[] { DicomTag.SnoutPosition, null, RequirementCode.OneOne }; - yield return new object[] { DicomTag.SnoutPosition, null, RequirementCode.TwoOne }; - - // present, but zero length - yield return new object[] { DicomTag.PatientBirthName, new DicomPersonName(DicomTag.PatientBirthName), RequirementCode.OneOne }; - yield return new object[] { DicomTag.PatientBirthName, new DicomPersonName(DicomTag.PatientBirthName), RequirementCode.ThreeOne }; - yield return new object[] { DicomTag.PatientBirthName, new DicomPersonName(DicomTag.PatientBirthName), RequirementCode.ThreeTwo }; - yield return new object[] { DicomTag.PatientBirthName, new DicomPersonName(DicomTag.PatientBirthName), RequirementCode.ThreeThree }; - yield return new object[] { DicomTag.PatientBirthName, new DicomPersonName(DicomTag.PatientBirthName), RequirementCode.OneCOne }; - yield return new object[] { DicomTag.PatientBirthName, new DicomPersonName(DicomTag.PatientBirthName), RequirementCode.OneCOneC }; - yield return new object[] { DicomTag.PatientBirthName, new DicomPersonName(DicomTag.PatientBirthName), RequirementCode.OneCTwo }; - yield return new object[] { DicomTag.ApprovalStatusDateTime, new DicomDateTime(DicomTag.ApprovalStatusDateTime, new string[0]), RequirementCode.OneOne }; - yield return new object[] { DicomTag.ApprovalStatusDateTime, new DicomDateTime(DicomTag.ApprovalStatusDateTime, new string[0]), RequirementCode.ThreeOne }; - yield return new object[] { DicomTag.ApprovalStatusDateTime, new DicomDateTime(DicomTag.ApprovalStatusDateTime, new string[0]), RequirementCode.ThreeTwo }; - yield return new object[] { DicomTag.ApprovalStatusDateTime, new DicomDateTime(DicomTag.ApprovalStatusDateTime, new string[0]), RequirementCode.ThreeThree }; - yield return new object[] { DicomTag.ApprovalStatusDateTime, new DicomDateTime(DicomTag.ApprovalStatusDateTime, new string[0]), RequirementCode.OneCOne }; - yield return new object[] { DicomTag.ApprovalStatusDateTime, new DicomDateTime(DicomTag.ApprovalStatusDateTime, new string[0]), RequirementCode.OneCOneC }; - yield return new object[] { DicomTag.ApprovalStatusDateTime, new DicomDateTime(DicomTag.ApprovalStatusDateTime, new string[0]), RequirementCode.OneCTwo }; - yield return new object[] { DicomTag.SelectorSLValue, new DicomSignedLong(DicomTag.SelectorSLValue), RequirementCode.OneOne }; - yield return new object[] { DicomTag.SelectorSLValue, new DicomSignedLong(DicomTag.SelectorSLValue), RequirementCode.ThreeOne }; - yield return new object[] { DicomTag.SelectorSLValue, new DicomSignedLong(DicomTag.SelectorSLValue), RequirementCode.ThreeTwo }; - yield return new object[] { DicomTag.SelectorSLValue, new DicomSignedLong(DicomTag.SelectorSLValue), RequirementCode.ThreeThree }; - yield return new object[] { DicomTag.SelectorSLValue, new DicomSignedLong(DicomTag.SelectorSLValue), RequirementCode.OneCOne }; - yield return new object[] { DicomTag.SelectorSLValue, new DicomSignedLong(DicomTag.SelectorSLValue), RequirementCode.OneCOneC }; - yield return new object[] { DicomTag.SelectorSLValue, new DicomSignedLong(DicomTag.SelectorSLValue), RequirementCode.OneCTwo }; - yield return new object[] { DicomTag.SnoutSequence, new DicomSequence(DicomTag.SnoutSequence), RequirementCode.OneOne }; - yield return new object[] { DicomTag.SnoutSequence, new DicomSequence(DicomTag.SnoutSequence), RequirementCode.ThreeOne }; - yield return new object[] { DicomTag.SnoutSequence, new DicomSequence(DicomTag.SnoutSequence), RequirementCode.ThreeTwo }; - yield return new object[] { DicomTag.SnoutSequence, new DicomSequence(DicomTag.SnoutSequence), RequirementCode.ThreeThree }; - yield return new object[] { DicomTag.SnoutSequence, new DicomSequence(DicomTag.SnoutSequence), RequirementCode.OneCOne }; - yield return new object[] { DicomTag.SnoutSequence, new DicomSequence(DicomTag.SnoutSequence), RequirementCode.OneCOneC }; - yield return new object[] { DicomTag.SnoutSequence, new DicomSequence(DicomTag.SnoutSequence), RequirementCode.OneCTwo }; - - // present and has some value - yield return new object[] { DicomTag.PatientBirthName, new DicomPersonName(DicomTag.PatientBirthName, "foo"), RequirementCode.MustBeEmpty }; - yield return new object[] { DicomTag.ApprovalStatusDateTime, new DicomDateTime(DicomTag.ApprovalStatusDateTime, DateTime.UtcNow), RequirementCode.MustBeEmpty }; - yield return new object[] { DicomTag.SelectorSLValue, new DicomSignedLong(DicomTag.SelectorSLValue, 0), RequirementCode.MustBeEmpty }; - yield return new object[] { DicomTag.SnoutSequence, new DicomSequence(DicomTag.SnoutSequence, new DicomDataset[] { new DicomDataset(new DicomDecimalString(DicomTag.PixelBandwidth, "1.0")) }), RequirementCode.MustBeEmpty }; - - // present - yield return new object[] { DicomTag.PatientBirthName, new DicomPersonName(DicomTag.PatientBirthName), RequirementCode.NotAllowed }; - yield return new object[] { DicomTag.PatientBirthName, new DicomPersonName(DicomTag.PatientBirthName, "foo"), RequirementCode.NotAllowed }; - yield return new object[] { DicomTag.ApprovalStatusDateTime, new DicomDateTime(DicomTag.ApprovalStatusDateTime, new string[0]), RequirementCode.NotAllowed }; - yield return new object[] { DicomTag.ApprovalStatusDateTime, new DicomDateTime(DicomTag.ApprovalStatusDateTime, DateTime.UtcNow), RequirementCode.NotAllowed }; - yield return new object[] { DicomTag.SelectorSLValue, new DicomSignedLong(DicomTag.SelectorSLValue), RequirementCode.NotAllowed }; - yield return new object[] { DicomTag.SelectorSLValue, new DicomSignedLong(DicomTag.SelectorSLValue, 0), RequirementCode.NotAllowed }; - yield return new object[] { DicomTag.SnoutSequence, new DicomSequence(DicomTag.SnoutSequence), RequirementCode.NotAllowed }; - yield return new object[] { DicomTag.SnoutSequence, new DicomSequence(DicomTag.SnoutSequence, new DicomDataset[] { new DicomDataset(new DicomDecimalString(DicomTag.PixelBandwidth, "1.0")) }), RequirementCode.NotAllowed }; - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Extensions/DicomElementExtensionsTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Extensions/DicomElementExtensionsTests.cs deleted file mode 100644 index 8544f38865..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Extensions/DicomElementExtensionsTests.cs +++ /dev/null @@ -1,43 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using FellowOakDicom; -using FellowOakDicom.IO; -using Microsoft.Health.Dicom.Core.Extensions; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Extensions; - -public class DicomElementExtensionsTests -{ - - [Fact] - public void GivenDicomElementWithMultipleValues_WhenCallGetFirstValueOrDefault_ThenShouldReturnFirstOne() - { - DicomElement element = new DicomLongString(DicomTag.StudyDescription, "Value1", "Value2"); - Assert.Equal("Value1", element.GetFirstValueOrDefault()); - } - - [Fact] - public void GivenDicomElementWithMultipleBinaryValues_WhenCallGetFirstValueOrDefault_ThenShouldReturnFirstOne() - { - DicomElement element = new DicomSignedShort(DicomTag.LargestImagePixelValue, ByteConverter.ToByteBuffer(new byte[] { 1, 2, 3 })); - Assert.Equal(513, element.GetFirstValueOrDefault()); - } - - [Fact] - public void GivenDicomElementWithSingleValue_WhenCallGetFirstValueOrDefault_ThenShouldReturnFirstOne() - { - DicomElement element = new DicomSignedShort(DicomTag.ExposureControlSensingRegionLeftVerticalEdge, 1); - Assert.Equal(1, element.GetFirstValueOrDefault()); - } - - [Fact] - public void GivenDicomElementWithoutValues_WhenCallGetFirstValueOrDefault_ThenShouldReturnDefault() - { - DicomElement element = new DicomLongString(DicomTag.StudyDescription, string.Empty); - Assert.Null(element.GetFirstValueOrDefault()); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Extensions/DicomFileExtensionsTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Extensions/DicomFileExtensionsTests.cs deleted file mode 100644 index dc7d733991..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Extensions/DicomFileExtensionsTests.cs +++ /dev/null @@ -1,68 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; -using FellowOakDicom; -using Microsoft.Health.Dicom.Tests.Common; -using Microsoft.IO; -using Xunit; -using DicomFileExtensions = Microsoft.Health.Dicom.Core.Features.Retrieve.DicomFileExtensions; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Extensions; -public class DicomFileExtensionsTests -{ - private const string TestDataRootFolder = "TranscodingSamples"; - - [Fact] - public async Task GivenNullDicomFile_WhenGetDatasetLengthAsyncIsCalled_ThrowsArgumentNullException() - { - DicomFile dcmFile = null; - var recyclableMemoryStreamManager = new RecyclableMemoryStreamManager(); - - await Assert.ThrowsAsync(() => DicomFileExtensions.GetByteLengthAsync(dcmFile, recyclableMemoryStreamManager)); - } - - [Fact] - public async Task GivenNullRecyclableMemoryStreamManager_WhenGetDatasetLengthAsyncIsCalled_ThrowsArgumentNullException() - { - var dataset = new DicomDataset - { - { DicomTag.SOPClassUID, TestUidGenerator.Generate() }, - { DicomTag.SOPInstanceUID, TestUidGenerator.Generate() } - }; - var dcmFile = new DicomFile(dataset); - RecyclableMemoryStreamManager recyclableMemoryStreamManager = null; - - await Assert.ThrowsAsync(() => DicomFileExtensions.GetByteLengthAsync(dcmFile, recyclableMemoryStreamManager)); - } - - [Theory] - [MemberData(nameof(GetAllTestDatas))] - public async Task GivenDicomFile_WhenGetDatasetLengthAsyncIsCalled_LengthShouldMatch(string fileName) - { - long expectedLength = 0; - var inFile = DicomFile.Open(fileName); - - using (var stream = new MemoryStream()) - { - await inFile.SaveAsync(stream); - expectedLength = stream.Length; - } - - var length = await DicomFileExtensions.GetByteLengthAsync(inFile, new RecyclableMemoryStreamManager()); - Assert.Equal(expectedLength, length); - } - - public static IEnumerable GetAllTestDatas() - { - foreach (string path in Directory.EnumerateFiles(TestDataRootFolder, "*", SearchOption.AllDirectories)) - { - yield return new object[] { path }; - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Extensions/DicomTagExtensionsTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Extensions/DicomTagExtensionsTests.cs deleted file mode 100644 index dfaa515c5f..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Extensions/DicomTagExtensionsTests.cs +++ /dev/null @@ -1,35 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Extensions; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Extensions; - -public class DicomTagExtensionsTests -{ - [Fact] - public void GivenValidDicomTag_WhenGetPath_ThenShouldReturnCorrectValue() - { - Assert.Equal("0014408B", DicomTag.UserSelectedGainY.GetPath()); - } - - [Theory] - [MemberData(nameof(MemberDataForTestingGetDefaultVR))] - public void GivenDicomTag_WhenGetDefaultVR_ThenShouldReturnExpectedValue(DicomTag dicomTag, DicomVR expectedVR) - { - Assert.Equal(expectedVR, dicomTag.GetDefaultVR()); - } - - public static IEnumerable MemberDataForTestingGetDefaultVR() - { - yield return new object[] { DicomTag.StudyInstanceUID, DicomVR.UI }; // standard DicomTag - yield return new object[] { DicomTag.Parse("12051003"), null }; // private DicomTag - yield return new object[] { DicomTag.Parse("22010010"), DicomVR.LO }; // private identification code - yield return new object[] { DicomTag.Parse("0018B001"), null }; // invalid DicomTag - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Extensions/ExtendedQueryTagEntryExtensionsTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Extensions/ExtendedQueryTagEntryExtensionsTests.cs deleted file mode 100644 index 4a483ffe41..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Extensions/ExtendedQueryTagEntryExtensionsTests.cs +++ /dev/null @@ -1,122 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Tests.Common.Extensions; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Extensions; - -public class ExtendedQueryTagEntryExtensionsTests -{ - [Theory] - [MemberData(nameof(GetValidExtendedQueryTagEntries))] - public void GivenValidExtendedQueryTagEntry_WhenNormalizing_ThenShouldReturnSameEntry(AddExtendedQueryTagEntry entry) - { - AddExtendedQueryTagEntry normalized = entry.Normalize(); - Assert.Equal(entry.Path, normalized.Path); - Assert.Equal(entry.VR, normalized.VR); - Assert.Equal(entry.Level, normalized.Level); - } - - [Theory] - [InlineData("")] - [InlineData(null)] - [InlineData(" ")] - public void GivenPrivateTagWithNonEmptyPrivateCreator_WhenNormalizing_ThenPrivateCreatorShouldBeNull(string privateCreator) - { - DicomTag tag1 = new DicomTag(0x0405, 0x1001); - AddExtendedQueryTagEntry normalized = new AddExtendedQueryTagEntry() { Level = QueryTagLevel.Instance, Path = tag1.GetPath(), PrivateCreator = privateCreator, VR = DicomVRCode.CS }.Normalize(); - Assert.Null(normalized.PrivateCreator); - } - - [Fact] - - public void GivenPrivateIdentificationCodeTagWithoutVR_WhenNormalizing_ThenVRShouldBeFilled() - { - AddExtendedQueryTagEntry entry = CreateExtendedQueryTagEntry("22010010", null, null, QueryTagLevel.Instance); - AddExtendedQueryTagEntry normalized = entry.Normalize(); - Assert.Equal(DicomVRCode.LO, normalized.VR); - } - - [Theory] - [InlineData(null)] - [InlineData("")] - - public void GivenStandardTagWithoutVR_WhenNormalizing_ThenVRShouldBeFilled(string vr) - { - DicomTag tag = DicomTag.DeviceSerialNumber; - AddExtendedQueryTagEntry entry = CreateExtendedQueryTagEntry(tag.GetPath(), vr, null, QueryTagLevel.Instance); - AddExtendedQueryTagEntry normalized = entry.Normalize(); - Assert.Equal(tag.GetDefaultVR().Code, normalized.VR); - } - - [Fact] - public void GivenStandardTagWithVR_WhenNormalizing_ThenVRShouldNotBeUpdated() - { - DicomTag tag = DicomTag.DeviceSerialNumber; - string vr = DicomVR.CS.Code; - AddExtendedQueryTagEntry entry = CreateExtendedQueryTagEntry(tag.GetPath(), vr, null, QueryTagLevel.Instance); - AddExtendedQueryTagEntry normalized = entry.Normalize(); - Assert.Equal(vr, normalized.VR); - } - - [Fact] - public void GivenTagOfLowerCase_WhenNormalizing_ThenTagShouldBeUpperCase() - { - DicomTag tag = DicomTag.DeviceLabel; - AddExtendedQueryTagEntry entry = CreateExtendedQueryTagEntry(tag.GetPath().ToLowerInvariant(), tag.GetDefaultVR().Code, null, QueryTagLevel.Instance); - AddExtendedQueryTagEntry normalized = entry.Normalize(); - Assert.Equal(entry.Path.ToUpperInvariant(), normalized.Path); - } - - [Fact] - public void GivenVROfLowerCase_WhenNormalizing_ThenVRShouldBeUpperCase() - { - DicomTag tag = DicomTag.DeviceLabel; - AddExtendedQueryTagEntry entry = CreateExtendedQueryTagEntry(tag.GetPath(), tag.GetDefaultVR().Code.ToLowerInvariant(), null, QueryTagLevel.Instance); - AddExtendedQueryTagEntry normalized = entry.Normalize(); - Assert.Equal(entry.VR.ToUpperInvariant(), normalized.VR); - } - - [Fact] - - public void GivenStandardTagAsKeyword_WhenNormalizing_ThenVRShouldBeFilled() - { - DicomTag tag = DicomTag.DeviceSerialNumber; - AddExtendedQueryTagEntry entry = CreateExtendedQueryTagEntry(path: tag.DictionaryEntry.Keyword, tag.GetDefaultVR().Code, null, QueryTagLevel.Instance); - string expectedPath = tag.GetPath(); - AddExtendedQueryTagEntry normalized = entry.Normalize(); - Assert.Equal(normalized.Path, expectedPath); - } - - [Fact] - - public void GivenInvalidTagWithoutVR_WhenNormalizing_ThenShouldNotThrowException() - { - // Add this unit test for regression: we had a bug when tag is valid and VR is null, NullPointerException is thrown. More details can be found https://microsofthealth.visualstudio.com/Health/_workitems/edit/81015 - AddExtendedQueryTagEntry entry = CreateExtendedQueryTagEntry(path: "00111011", null, null, QueryTagLevel.Series); - entry.Normalize(); - } - - public static IEnumerable GetValidExtendedQueryTagEntries() - { - yield return new object[] { DicomTag.DeviceSerialNumber.BuildAddExtendedQueryTagEntry() }; // standard extended query tag with VR - yield return new object[] { CreateExtendedQueryTagEntry("12051003", DicomVRCode.OB, "PrivateCreator1", QueryTagLevel.Instance) }; // private tag with VR - } - - private static GetExtendedQueryTagEntry CreateExtendedQueryTagEntry(string path, string vr, string privateCreator, QueryTagLevel level = QueryTagLevel.Instance, ExtendedQueryTagStatus status = ExtendedQueryTagStatus.Ready) - { - return new GetExtendedQueryTagEntry { Path = path, VR = vr, PrivateCreator = privateCreator, Level = level, Status = status }; - } - - private static AddExtendedQueryTagEntry CreateExtendedQueryTagEntry(string path, string vr, string privateCreator, QueryTagLevel level = QueryTagLevel.Instance) - { - return new AddExtendedQueryTagEntry { Path = path, VR = vr, PrivateCreator = privateCreator, Level = level }; - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Extensions/JsonSerializerOptionsExtensionsTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Extensions/JsonSerializerOptionsExtensionsTests.cs deleted file mode 100644 index 38abcd6821..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Extensions/JsonSerializerOptionsExtensionsTests.cs +++ /dev/null @@ -1,205 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Text.Json; -using System.Text.Json.Serialization; -using FellowOakDicom; -using Microsoft.Health.FellowOakDicom.Serialization; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Serialization; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Extensions; - -public class JsonSerializerOptionsExtensionsTests -{ - private readonly JsonSerializerOptions _options; - - public JsonSerializerOptionsExtensionsTests() - { - _options = new JsonSerializerOptions(); - _options.ConfigureDefaultDicomSettings(); - } - - [Fact] - public void GivenOptions_WhenConfiguringDefaults_ThenUpdateProperties() - { - Assert.Equal(4, _options.Converters.Count); - Assert.Equal(typeof(DicomIdentifierJsonConverter), _options.Converters[0].GetType()); - Assert.Equal(typeof(DicomJsonConverter), _options.Converters[1].GetType()); - Assert.Equal(typeof(ExportDataOptionsJsonConverter), _options.Converters[2].GetType()); - Assert.Equal(typeof(StrictStringEnumConverter), _options.Converters[3].GetType()); - - Assert.True(_options.AllowTrailingCommas); - Assert.Equal(JsonIgnoreCondition.WhenWritingNull, _options.DefaultIgnoreCondition); - Assert.Equal(JsonNamingPolicy.CamelCase, _options.DictionaryKeyPolicy); - Assert.Null(_options.Encoder); - Assert.False(_options.IgnoreReadOnlyFields); - Assert.False(_options.IgnoreReadOnlyProperties); - Assert.False(_options.IncludeFields); - Assert.Equal(0, _options.MaxDepth); - Assert.Equal(JsonNumberHandling.Strict, _options.NumberHandling); - Assert.True(_options.PropertyNameCaseInsensitive); - Assert.Equal(JsonNamingPolicy.CamelCase, _options.PropertyNamingPolicy); - Assert.Equal(JsonCommentHandling.Skip, _options.ReadCommentHandling); - Assert.False(_options.WriteIndented); - } - - [Fact] - public void GivenSerializationOptions_WhenDeserializingNumbers_ThenAcceptOnlyNumberTokens() - { - // PascalCase + trailing comma too! - const string json = @" -{ - ""Word"": ""Hello"", - ""Number"": 123, -} -"; - - Example actual = JsonSerializer.Deserialize(json, _options); - Assert.Equal("Hello", actual.Word); - Assert.Equal(123, actual.Number); - - // Invalid - Assert.Throws(() => JsonSerializer.Deserialize(@" -{ - ""Word"": ""World"", - ""Number"": ""456"", -} -", - _options)); - } - - [Theory] - [InlineData("50", 50)] - [InlineData("50.0", 50)] - [InlineData("\"50\"", 50)] - [InlineData("-1.23e4", -1.23e4)] - [InlineData("\"-1.23e4\"", -1.23e4)] - public void GivenSerializationOptions_WhenDeserializingDsVr_ThenAcceptEitherTokenKind(string weightJson, decimal expected) - { - string json = @$" -{{ - ""00101030"":{{ - ""vr"":""DS"", - ""Value"":[ - {weightJson} - ] - }} -}} -"; - DicomDataset actual = JsonSerializer.Deserialize(json, _options); - Assert.Equal(expected, actual.GetValue(DicomTag.PatientWeight, 0)); - } - - [Fact] - public void GivenSerializationOptions_WhenDeserializingDicomComments_ThenSkipThem() - { - const string json = @" -{ - // Study Date - ""00080020"":{ - ""vr"":""DA"", // Date - ""Value"":[ - ""20080701"" - ] - }, - // Modality - ""00080060"":{ - ""vr"":""CS"", // Code String - ""Value"":[ - ""XRAY"" - ] - }, -} -"; - - DicomDataset actual = JsonSerializer.Deserialize(json, _options); - Assert.Equal(new DateTime(2008, 07, 01), actual.GetValue(DicomTag.StudyDate, 0)); - Assert.Equal("XRAY", actual.GetValue(DicomTag.Modality, 0)); - } - - [Fact] - public void GivenSerializationOptions_WhenDeserializingCustomComments_ThenSkipThem() - { - const string json = @" -{ - // Comment one - ""Word"": ""Foo Bar"", // Comment two - ""Number"": 789, - // Comment three -} -"; - - _options.Converters.Add(new CommentlessConverter()); - Example ex = JsonSerializer.Deserialize(json, _options); - Assert.Equal("Foo Bar", ex.Word); - Assert.Equal(789, ex.Number); - } - - private sealed class Example - { - public string Word { get; set; } - - public int Number { get; set; } - } - - private sealed class CommentlessConverter : JsonConverter - { - public override Example Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType != JsonTokenType.StartObject) - { - throw new JsonException(); - } - - StringComparison comparison = options.PropertyNameCaseInsensitive - ? StringComparison.OrdinalIgnoreCase - : StringComparison.Ordinal; - - var example = new Example(); - while (reader.Read()) - { - switch (reader.TokenType) - { - case JsonTokenType.PropertyName: - // Get property name - string name = reader.GetString(); - if (!reader.Read()) - { - throw new JsonException(); - } - - // Assign property - if (string.Equals(name, nameof(Example.Word), comparison)) - { - example.Word = reader.GetString(); - } - else if (string.Equals(name, nameof(Example.Number), comparison)) - { - example.Number = reader.GetInt32(); - } - else - { - throw new JsonException(); - } - break; - case JsonTokenType.EndObject: - return example; - default: - throw new JsonException(); - } - } - - throw new JsonException(); - } - - public override void Write(Utf8JsonWriter writer, Example value, JsonSerializerOptions options) - { - throw new NotImplementedException(); - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Extensions/JsonSerializerSettingsExtensionsTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Extensions/JsonSerializerSettingsExtensionsTests.cs deleted file mode 100644 index d717e6c2ab..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Extensions/JsonSerializerSettingsExtensionsTests.cs +++ /dev/null @@ -1,116 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Serialization.Newtonsoft; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Newtonsoft.Json.Serialization; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Extensions; - -public class JsonSerializerSettingsExtensionsTests -{ - private readonly JsonSerializerSettings _settings; - - public JsonSerializerSettingsExtensionsTests() - { - _settings = new JsonSerializerSettings(); - _settings.ConfigureDefaultDicomSettings(); - } - - [Fact] - public void GivenSettings_WhenConfiguringDefaults_ThenUpdateProperties() - { - Assert.Equal(4, _settings.Converters.Count); - Assert.Equal(typeof(DicomIdentifierJsonConverter), _settings.Converters[0].GetType()); - Assert.Equal(typeof(ExportDestinationOptionsJsonConverter), _settings.Converters[1].GetType()); - Assert.Equal(typeof(ExportSourceOptionsJsonConverter), _settings.Converters[2].GetType()); - Assert.Equal(typeof(StringEnumConverter), _settings.Converters[3].GetType()); - - Assert.Equal(DateParseHandling.None, _settings.DateParseHandling); - Assert.Equal(TypeNameHandling.None, _settings.TypeNameHandling); - Assert.IsType(_settings.ContractResolver); - Assert.IsType(((DefaultContractResolver)_settings.ContractResolver).NamingStrategy); - } - - [Fact] - public void GivenOldJson_WhenDeserializingWithNewSettings_ThenPreserveProperties() - { - const string json = -@"{ - ""Timestamp"": ""2022-05-09T16:56:48.3050668Z"", - ""Options"": { - ""Enabled"": true, - ""Number"": 42, - ""Words"": [ - ""foo"", - ""bar"", - ""baz"" - ], - ""Extra"": null, - }, - ""Href"": ""https://www.bing.com"" -}"; - - Checkpoint checkpoint = JsonConvert.DeserializeObject(json, _settings); - AssertCheckpoint(checkpoint); - - string actual = JsonConvert.SerializeObject(checkpoint, Formatting.Indented, _settings); - Assert.Equal( -@"{ - ""timestamp"": ""2022-05-09T16:56:48.3050668Z"", - ""options"": { - ""enabled"": true, - ""number"": 42, - ""words"": [ - ""foo"", - ""bar"", - ""baz"" - ] - }, - ""href"": ""https://www.bing.com"" -}", - actual); - - checkpoint = JsonConvert.DeserializeObject(actual, _settings); - AssertCheckpoint(checkpoint); - } - - private static void AssertCheckpoint(Checkpoint checkpoint) - { - var timestamp = DateTime.Parse("2022-05-09T16:56:48.3050668Z", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal); - Assert.Equal(timestamp, checkpoint.Timestamp); - Assert.Equal(new Uri("https://www.bing.com"), checkpoint.Href); - Assert.True(checkpoint.Options.Enabled); - Assert.Equal(42, checkpoint.Options.Number); - Assert.True(checkpoint.Options.Words.SequenceEqual(new string[] { "foo", "bar", "baz" })); - } - - private sealed class Checkpoint - { - public DateTime Timestamp { get; set; } - - public Options Options { get; set; } - - public Uri Href { get; set; } - } - - private sealed class Options - { - public bool Enabled { get; set; } - - public int Number { get; set; } - - public IReadOnlyList Words { get; set; } - - public object Extra { get; set; } - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Extensions/ValidationErrorCodeExtensionsTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Extensions/ValidationErrorCodeExtensionsTests.cs deleted file mode 100644 index 4a1504ce0a..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Extensions/ValidationErrorCodeExtensionsTests.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Validation; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Extensions; - -public class ValidationErrorCodeExtensionsTests -{ - [Fact] - public void GivenAnyErrorCode_WhenGetMessage_ThenShouldReturnValue() - { - foreach (var value in Enum.GetValues(typeof(ValidationErrorCode))) - { - // if not exist, would throw exception - ((ValidationErrorCode)value).GetMessage(); - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/ChangeFeed/ChangeFeedServiceTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/ChangeFeed/ChangeFeedServiceTests.cs deleted file mode 100644 index 6259bc83bf..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/ChangeFeed/ChangeFeedServiceTests.cs +++ /dev/null @@ -1,140 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using FellowOakDicom; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Features.ChangeFeed; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Models; -using Microsoft.Health.Dicom.Tests.Common; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.ChangeFeed; - -public class ChangeFeedServiceTests -{ - private readonly IChangeFeedStore _changeFeedStore = Substitute.For(); - private readonly IMetadataStore _metadataStore = Substitute.For(); - private readonly ChangeFeedService _changeFeedService; - - public ChangeFeedServiceTests() - { - // For the test, we'll use unbounded parallelism with -1 - _changeFeedService = new ChangeFeedService( - _changeFeedStore, - _metadataStore, - Options.Create(new RetrieveConfiguration { MaxDegreeOfParallelism = -1 })); - } - - [Fact] - public async Task GivenChangeFeed_WhenFetchingWithoutMetadata_ThenOnlyCheckStore() - { - const int offset = 10; - const int limit = 50; - const ChangeFeedOrder order = ChangeFeedOrder.Sequence; - TimeRange range = TimeRange.MaxValue; - var expected = new List - { - new ChangeFeedEntry(1, DateTime.Now, ChangeFeedAction.Create, TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 101, 101, ChangeFeedState.Current), - }; - - using var tokenSource = new CancellationTokenSource(); - - _changeFeedStore.GetChangeFeedAsync(range, offset, limit, order, tokenSource.Token).Returns(expected); - - IReadOnlyList actual = await _changeFeedService.GetChangeFeedAsync(range, offset, limit, order, false, tokenSource.Token); - - await _changeFeedStore.Received(1).GetChangeFeedAsync(range, offset, limit, order, tokenSource.Token); - await _metadataStore.DidNotReceiveWithAnyArgs().GetInstanceMetadataAsync(default, default); - - Assert.Same(expected, actual); - Assert.True(actual.All(x => !x.IncludeMetadata)); - Assert.True(actual.All(x => x.Metadata == null)); - } - - [Fact] - public async Task GivenChangeFeed_WhenFetchingWithMetadata_ThenFetchMetadataToo() - { - const int offset = 10; - const int limit = 50; - const ChangeFeedOrder order = ChangeFeedOrder.Time; - var range = new TimeRange(DateTimeOffset.UtcNow, DateTime.UtcNow.AddHours(1)); - var expected = new List - { - new ChangeFeedEntry(1, DateTime.Now, ChangeFeedAction.Create, TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 101, 101, ChangeFeedState.Current), - new ChangeFeedEntry(2, DateTime.Now, ChangeFeedAction.Create, TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 102, null, ChangeFeedState.Deleted), - new ChangeFeedEntry(3, DateTime.Now, ChangeFeedAction.Create, TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 103, 104, ChangeFeedState.Replaced), - }; - var expectedDataset1 = new DicomDataset(); - var expectedDataset3 = new DicomDataset(); - - using var tokenSource = new CancellationTokenSource(); - - // Note: Parallel.ForEachAsync uses its own CancellationToken - _changeFeedStore.GetChangeFeedAsync(range, offset, limit, order, tokenSource.Token).Returns(expected); - _metadataStore.GetInstanceMetadataAsync(101, Arg.Any()).Returns(expectedDataset1); - _metadataStore.GetInstanceMetadataAsync(104, Arg.Any()).Returns(expectedDataset3); - - IReadOnlyList actual = await _changeFeedService.GetChangeFeedAsync(range, offset, limit, order, true, tokenSource.Token); - - await _changeFeedStore.Received(1).GetChangeFeedAsync(range, offset, limit, order, tokenSource.Token); - await _metadataStore.Received(1).GetInstanceMetadataAsync(101, Arg.Any()); - await _metadataStore.DidNotReceive().GetInstanceMetadataAsync(102, tokenSource.Token); - await _metadataStore.Received(1).GetInstanceMetadataAsync(104, Arg.Any()); - - Assert.Same(expected, actual); - Assert.True(actual[0].IncludeMetadata); - Assert.Same(expectedDataset1, actual[0].Metadata); - Assert.False(actual[1].IncludeMetadata); - Assert.Null(expected[1].Metadata); - Assert.True(actual[2].IncludeMetadata); - Assert.Same(expectedDataset3, actual[2].Metadata); - } - - [Fact] - public async Task GivenChangeFeed_WhenFetchingLatestWithoutMetadata_ThenOnlyCheckStore() - { - const ChangeFeedOrder order = ChangeFeedOrder.Time; - var expected = new ChangeFeedEntry(1, DateTime.Now, ChangeFeedAction.Create, TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 101, 101, ChangeFeedState.Current); - using var tokenSource = new CancellationTokenSource(); - - _changeFeedStore.GetChangeFeedLatestAsync(order, tokenSource.Token).Returns(expected); - - ChangeFeedEntry actual = await _changeFeedService.GetChangeFeedLatestAsync(order, false, tokenSource.Token); - - await _changeFeedStore.Received(1).GetChangeFeedLatestAsync(order, tokenSource.Token); - await _metadataStore.DidNotReceiveWithAnyArgs().GetInstanceMetadataAsync(default, default); - - Assert.Same(expected, actual); - Assert.False(actual.IncludeMetadata); - Assert.Null(actual.Metadata); - } - - [Fact] - public async Task GivenChangeFeed_WhenFetchingLatestDeletedWithMetadata_ThenOnlyCheckStore() - { - const ChangeFeedOrder order = ChangeFeedOrder.Sequence; - var expected = new ChangeFeedEntry(1, DateTime.Now, ChangeFeedAction.Create, TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 101, null, ChangeFeedState.Deleted); - using var tokenSource = new CancellationTokenSource(); - - _changeFeedStore.GetChangeFeedLatestAsync(order, tokenSource.Token).Returns(expected); - - ChangeFeedEntry actual = await _changeFeedService.GetChangeFeedLatestAsync(order, true, tokenSource.Token); - - await _changeFeedStore.Received(1).GetChangeFeedLatestAsync(order, tokenSource.Token); - await _metadataStore.DidNotReceiveWithAnyArgs().GetInstanceMetadataAsync(default, default); - - Assert.Same(expected, actual); - Assert.False(actual.IncludeMetadata); - Assert.Null(actual.Metadata); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Common/DicomTagParserTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Common/DicomTagParserTests.cs deleted file mode 100644 index 9b9d5b7a2b..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Common/DicomTagParserTests.cs +++ /dev/null @@ -1,70 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Common; - -public class DicomTagParserTests -{ - private readonly IDicomTagParser _dicomTagParser; - - public DicomTagParserTests() - { - _dicomTagParser = new DicomTagParser(); - } - - [MemberData(nameof(GetValidTags))] - [Theory] - public void GivenValidTag_WhenParse_ThenShouldReturnCorrectValue(string dicomTagPath, DicomTag[] expectedTags, bool supportMultiple = false) - { - DicomTag[] tags; - bool succeed = _dicomTagParser.TryParse(dicomTagPath, out tags, supportMultiple); - Assert.True(succeed); - for (int i = 0; i < expectedTags.Length; i++) - { - Assert.True(expectedTags[i].Equals(tags[i])); - } - } - - [MemberData(nameof(GetInvalidTags))] - [Theory] - public void GivenInvalidTag_WhenParse_ThenShouldReturnFalse(string dicomTagPath, bool supportMultiple = false) - { - DicomTag[] tags; - bool succeed = _dicomTagParser.TryParse(dicomTagPath, out tags, supportMultiple); - Assert.False(succeed); - } - - public static IEnumerable GetValidTags() - { - yield return new object[] { DicomTag.AcquisitionDateTime.GetPath(), new DicomTag[] { DicomTag.AcquisitionDateTime } }; // attribute id - yield return new object[] { DicomTag.AcquisitionDateTime.GetPath().ToLowerInvariant(), new DicomTag[] { DicomTag.AcquisitionDateTime } }; // attribute id on lower case - yield return new object[] { DicomTag.AcquisitionDateTime.DictionaryEntry.Keyword, new DicomTag[] { DicomTag.AcquisitionDateTime } }; // keyword - yield return new object[] { "12051003", new DicomTag[] { DicomTag.Parse("12051003") } }; // private tag - yield return new object[] { "24010010", new DicomTag[] { DicomTag.Parse("24010010") } }; // Private Identification code - yield return new object[] { "0040A370.00080050", new DicomTag[] { DicomTag.ReferencedRequestSequence, DicomTag.AccessionNumber }, true }; // ReferencedRequestSequence.Accesionnumber - yield return new object[] { "0040A370.00401001", new DicomTag[] { DicomTag.ReferencedRequestSequence, DicomTag.Requested​Procedure​ID }, true }; // ReferencedRequestSequence.Requested​Procedure​ID - yield return new object[] { "24010010.12051003", new DicomTag[] { DicomTag.Parse("24010010"), DicomTag.Parse("12051003") }, true }; // Private - } - - public static IEnumerable GetInvalidTags() - { - yield return new object[] { string.Empty }; // empty - yield return new object[] { null }; // attribute id on lower case - yield return new object[] { DicomTag.AcquisitionDateTime.DictionaryEntry.Keyword.ToLowerInvariant() }; // keyword in lower case - yield return new object[] { "0018B001" }; // unknown tag - yield return new object[] { "0018B001A1" }; // longer than 8. - yield return new object[] { "Unknown" }; // bug https://microsofthealth.visualstudio.com/Health/_workitems/edit/80766 - yield return new object[] { "PrivateCreator" }; // Key word to Private Identification code. - yield return new object[] { ".", true }; // delimiter only - yield return new object[] { "asdasdas.asdasdasd", true }; // invalid multiple tags - yield return new object[] { "0040A370.asdasdasd", true }; // valid first level tag and invalid second level tag - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Common/EphemeralMemoryCacheTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Common/EphemeralMemoryCacheTests.cs deleted file mode 100644 index 6061a43741..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Common/EphemeralMemoryCacheTests.cs +++ /dev/null @@ -1,63 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Features.Common; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Common; -public class EphemeralMemoryCacheTests -{ - [Fact] - public async Task GivenCacheImp_MultipleThreadsSameGet_GetFuncExecutedOnce() - { - var config = Substitute.For>(); - config.Value.Returns(new CacheConfiguration() { MaxCacheSize = 10, MaxCacheAbsoluteExpirationInMinutes = 1 }); - var cache = new TestEphemeralMemoryCache(config, Substitute.For(), Substitute.For>()); - - int numExecuted = 0; - Func> mockAction = async (int input, CancellationToken cancellationToken) => - { - await Task.Delay(10, cancellationToken); - numExecuted++; - return 1; - }; - - var threadList = Enumerable.Range(0, 3).Select(async _ => await cache.GetAsync(1, 1, mockAction, CancellationToken.None)); - await Task.WhenAll(threadList); - - Assert.Equal(1, numExecuted); - } - - [Fact] - public async Task GivenCacheImp_WithFuncResultNull_ReturnsNull() - { - var config = Substitute.For>(); - config.Value.Returns(new CacheConfiguration()); - var cache = new TestEphemeralMemoryCache(config, Substitute.For(), Substitute.For>()); - - Func> mockAction = async (int input, CancellationToken cancellationToken) => - { - await Task.Run(() => 1); - return null; - }; - Assert.Null(await cache.GetAsync(1, 1, mockAction, CancellationToken.None)); - } - - public class TestEphemeralMemoryCache : EphemeralMemoryCache - { - public TestEphemeralMemoryCache(IOptions configuration, ILoggerFactory loggerFactory, ILogger logger) - : base(configuration, loggerFactory, logger) - { - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Common/ParallelEnumerableTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Common/ParallelEnumerableTests.cs deleted file mode 100644 index a26b4ac063..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Common/ParallelEnumerableTests.cs +++ /dev/null @@ -1,117 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Health.Dicom.Core.Features.Common; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Common; - -public class ParallelEnumerableTests -{ - [Fact] - public async Task GivenInvalidInput_WhenSelectingInParallel_ThenThrowArgumentException() - { - var input = new List { "1", "2", "3" }; - - await Assert.ThrowsAsync(() => ((List)null).SelectParallel(ParseAsync, new ParallelEnumerationOptions()).ToListAsync().AsTask()); - await Assert.ThrowsAsync(() => input.SelectParallel((Func>)null, new ParallelEnumerationOptions()).ToListAsync().AsTask()); - await Assert.ThrowsAsync(() => input.SelectParallel(ParseAsync, null).ToListAsync().AsTask()); - } - - [Fact] - public async Task GivenNoElements_WhenSelectingInParallel_ThenReturnEmpty() - { - var input = Enumerable.Empty(); - Assert.Empty(await input.SelectParallel(ParseAsync, new ParallelEnumerationOptions()).ToListAsync()); - } - - [Fact] - public async Task GivenOneElement_WhenSelectingInParallel_ThenReturnOneElement() - { - var input = new List { "42" }; - Assert.Equal(42, await input.SelectParallel(ParseAsync, new ParallelEnumerationOptions()).SingleAsync()); - } - - [Fact] - public async Task GivenFewerThenParallelism_WhenSelectingInParallel_ThenReturnAllResults() - { - var input = new List { "1", "2", "3" }; - await AssertValuesAsync( - input.SelectParallel(ParseAsync, new ParallelEnumerationOptions { MaxDegreeOfParallelism = input.Count + 1 }), - 1, 2, 3); - } - - [Fact] - public async Task GivenMoreThenParallelism_WhenSelectingInParallel_ThenReturnAllResults() - { - var input = new List { "1", "2", "3", "4", "5" }; - await AssertValuesAsync( - input.SelectParallel(ParseAsync, new ParallelEnumerationOptions { MaxDegreeOfParallelism = input.Count - 2 }), - 1, 2, 3, 4, 5); - } - - [Fact] - public async Task GivenSource_WhenEnumeratingMultipleTimes_ThenReturnSameishResults() - { - var input = new List { "1", "2", "3", "4", "5" }; - for (int i = 0; i < 5; i++) - { - await AssertValuesAsync( - input.SelectParallel(ParseAsync, new ParallelEnumerationOptions { MaxDegreeOfParallelism = input.Count - 2 }), - 1, 2, 3, 4, 5); - } - } - - [Fact] - public async Task GivenError_WhenSelectingInParallel_ThenRethrowError() - { - var input = new List { "1", "foo", "3" }; - await Assert.ThrowsAsync( - () => input.SelectParallel(ParseAsync, new ParallelEnumerationOptions()).ToListAsync().AsTask()); - } - - [Fact] - public async Task GivenCancelledToken_WhenSelectingInParallel_ThenThrowException() - { - using var tokenSource = new CancellationTokenSource(); - - int count = 0; - var input = new List { "1", "2", "3", "4", "5" }; - await Assert.ThrowsAsync( - () => input - .SelectParallel( - (x, t) => - { - if (Interlocked.Increment(ref count) == 4) - tokenSource.Cancel(); - - return ParseAsync(x, t); - }, - new ParallelEnumerationOptions { MaxDegreeOfParallelism = 2 }, - tokenSource.Token) - .ToListAsync() - .AsTask()); - } - - private static ValueTask ParseAsync(string s, CancellationToken cancellationToken) - => new ValueTask(Task.Run(() => int.Parse(s, CultureInfo.InvariantCulture), cancellationToken)); - - private static async ValueTask AssertValuesAsync(IAsyncEnumerable actual, params T[] expected) - { - HashSet set = await actual.ToHashSetAsync(); - - Assert.Equal(expected.Length, set.Count); - foreach (T e in expected) - { - Assert.True(set.Remove(e)); - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Delete/DeleteServiceTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Delete/DeleteServiceTests.cs deleted file mode 100644 index 0cc1f50afc..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Delete/DeleteServiceTests.cs +++ /dev/null @@ -1,621 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.ApplicationInsights; -using Microsoft.ApplicationInsights.Extensibility; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -#if NET8_0_OR_GREATER -using Microsoft.Extensions.Time.Testing; -#endif -using Microsoft.Health.Abstractions.Features.Transactions; -#if !NET8_0_OR_GREATER -using Microsoft.Health.Core.Internal; -#endif -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Context; -using Microsoft.Health.Dicom.Core.Features.Delete; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Core.Features.Store; -using Microsoft.Health.Dicom.Core.Models.Delete; -using Microsoft.Health.Dicom.Tests.Common; -using NSubstitute; -using NSubstitute.ExceptionExtensions; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Delete; - -public class DeleteServiceTests -{ - private readonly DeleteService _deleteService; - private readonly DeleteService _deleteServiceWithExternalStore; - private readonly IIndexDataStore _indexDataStore; - private readonly IFileStore _fileDataStore; - private readonly ITransactionScope _transactionScope; - private readonly DeletedInstanceCleanupConfiguration _deleteConfiguration; - private readonly IMetadataStore _metadataStore; - private readonly IDicomRequestContextAccessor _dicomRequestContextAccessor; - private readonly TelemetryClient _telemetryClient; - private readonly FileProperties _defaultFileProperties = new FileProperties - { - Path = "partitionA/123.dcm", - ETag = "e45678", - ContentLength = 123 - }; - -#if NET8_0_OR_GREATER - private readonly FakeTimeProvider _timeProvider = new FakeTimeProvider(DateTimeOffset.UtcNow); -#endif - - public DeleteServiceTests() - { - _telemetryClient = new TelemetryClient(new TelemetryConfiguration()); - _indexDataStore = Substitute.For(); - _metadataStore = Substitute.For(); - _fileDataStore = Substitute.For(); - _deleteConfiguration = new DeletedInstanceCleanupConfiguration - { - DeleteDelay = TimeSpan.FromDays(1), - BatchSize = 10, - MaxRetries = 5, - PollingInterval = TimeSpan.FromSeconds(1), - RetryBackOff = TimeSpan.FromDays(4), - }; - - IOptions deletedInstanceCleanupConfigurationOptions = Substitute.For>(); - deletedInstanceCleanupConfigurationOptions.Value.Returns(_deleteConfiguration); - ITransactionHandler transactionHandler = Substitute.For(); - _transactionScope = Substitute.For(); - transactionHandler.BeginTransaction().Returns(_transactionScope); - _dicomRequestContextAccessor = Substitute.For(); - _dicomRequestContextAccessor.RequestContext.DataPartition = Partition.Default; - - IOptions _options = Substitute.For>(); - _options.Value.Returns(new FeatureConfiguration { EnableExternalStore = false }); - _deleteService = new DeleteService( - _indexDataStore, - _metadataStore, - _fileDataStore, - deletedInstanceCleanupConfigurationOptions, - transactionHandler, - NullLogger.Instance, - _dicomRequestContextAccessor, - _options, -#if NET8_0_OR_GREATER - _telemetryClient, - _timeProvider); -#else - _telemetryClient); -#endif - - IOptions _optionsExternalStoreEnabled = Substitute.For>(); - _optionsExternalStoreEnabled.Value.Returns(new FeatureConfiguration { EnableExternalStore = true, }); - _deleteServiceWithExternalStore = new DeleteService( - _indexDataStore, - _metadataStore, - _fileDataStore, - deletedInstanceCleanupConfigurationOptions, - transactionHandler, - NullLogger.Instance, - _dicomRequestContextAccessor, - _optionsExternalStoreEnabled, -#if NET8_0_OR_GREATER - _telemetryClient, - _timeProvider); -#else - _telemetryClient); -#endif - } - - [Fact] - public async Task GivenADeleteStudyRequest_WhenDataStoreIsCalled_ThenCorrectDeleteDelayIsUsed() - { - string studyInstanceUid = TestUidGenerator.Generate(); - - DateTimeOffset now = DateTimeOffset.UtcNow; -#if NET8_0_OR_GREATER - _timeProvider.SetUtcNow(now); -#else - IDisposable replacement = Mock.Property(() => ClockResolver.UtcNowFunc, () => now); -#endif - await _deleteService.DeleteStudyAsync(studyInstanceUid, CancellationToken.None); - await _indexDataStore - .Received(1) - .DeleteStudyIndexAsync(Partition.Default, studyInstanceUid, now + _deleteConfiguration.DeleteDelay); - } - - [Fact] - public async Task GivenADeleteStudyRequest_WhenDataStoreWithExternalStoreIsCalled_ThenNoDelayIsUsed() - { - string studyInstanceUid = TestUidGenerator.Generate(); - - DateTimeOffset now = DateTimeOffset.UtcNow; -#if NET8_0_OR_GREATER - _timeProvider.SetUtcNow(now); -#else - IDisposable replacement = Mock.Property(() => ClockResolver.UtcNowFunc, () => now); -#endif - await _deleteServiceWithExternalStore.DeleteStudyAsync(studyInstanceUid, CancellationToken.None); - await _indexDataStore - .Received(1) - .DeleteStudyIndexAsync(Partition.Default, studyInstanceUid, now); - } - - [Fact] - public async Task GivenADeleteSeriesRequest_WhenDataStoreIsCalled_ThenCorrectDeleteDelayIsUsed() - { - string studyInstanceUid = TestUidGenerator.Generate(); - string seriesInstanceUid = TestUidGenerator.Generate(); - - DateTimeOffset now = DateTimeOffset.UtcNow; -#if NET8_0_OR_GREATER - _timeProvider.SetUtcNow(now); -#else - IDisposable replacement = Mock.Property(() => ClockResolver.UtcNowFunc, () => now); -#endif - await _deleteService.DeleteSeriesAsync(studyInstanceUid, seriesInstanceUid, CancellationToken.None); - await _indexDataStore - .Received(1) - .DeleteSeriesIndexAsync(Partition.Default, studyInstanceUid, seriesInstanceUid, now + _deleteConfiguration.DeleteDelay); - } - - [Fact] - public async Task GivenADeleteSeriesRequest_WhenDataStoreWithExternalStoreIsCalled_ThenNoDeleteDelayIsUsed() - { - string studyInstanceUid = TestUidGenerator.Generate(); - string seriesInstanceUid = TestUidGenerator.Generate(); - - DateTimeOffset now = DateTimeOffset.UtcNow; -#if NET8_0_OR_GREATER - _timeProvider.SetUtcNow(now); -#else - IDisposable replacement = Mock.Property(() => ClockResolver.UtcNowFunc, () => now); -#endif - await _deleteServiceWithExternalStore.DeleteSeriesAsync(studyInstanceUid, seriesInstanceUid, CancellationToken.None); - await _indexDataStore - .Received(1) - .DeleteSeriesIndexAsync(Partition.Default, studyInstanceUid, seriesInstanceUid, now); - } - - [Fact] - public async Task GivenADeleteInstanceRequest_WhenDataStoreIsCalled_ThenCorrectDeleteDelayIsUsed() - { - string studyInstanceUid = TestUidGenerator.Generate(); - string seriesInstanceUid = TestUidGenerator.Generate(); - string sopInstanceUid = TestUidGenerator.Generate(); - - DateTimeOffset now = DateTimeOffset.UtcNow; -#if NET8_0_OR_GREATER - _timeProvider.SetUtcNow(now); -#else - IDisposable replacement = Mock.Property(() => ClockResolver.UtcNowFunc, () => now); -#endif - await _deleteService.DeleteInstanceAsync(studyInstanceUid, seriesInstanceUid, sopInstanceUid, CancellationToken.None); - await _indexDataStore - .Received(1) - .DeleteInstanceIndexAsync(Partition.Default, studyInstanceUid, seriesInstanceUid, sopInstanceUid, now + _deleteConfiguration.DeleteDelay); - } - - [Fact] - public async Task GivenADeleteInstanceRequest_WhenNoInstancesFoundToDelete_ExpectNoExceptionsThrown() - { - string studyInstanceUid = TestUidGenerator.Generate(); - string seriesInstanceUid = TestUidGenerator.Generate(); - string sopInstanceUid = TestUidGenerator.Generate(); - - _indexDataStore - .DeleteInstanceIndexAsync(Partition.Default, studyInstanceUid, seriesInstanceUid, sopInstanceUid, Arg.Any(), Arg.Any()) - .Returns(Array.Empty()); - - await _deleteService.DeleteInstanceAsync(studyInstanceUid, seriesInstanceUid, sopInstanceUid, - CancellationToken.None); - } - - [Fact] - public async Task GivenADeleteInstanceRequest_WhenDataStoreWithExternalStoreIsCalled_ThenCorrectDelayIsUsed() - { - string studyInstanceUid = TestUidGenerator.Generate(); - string seriesInstanceUid = TestUidGenerator.Generate(); - string sopInstanceUid = TestUidGenerator.Generate(); - - DateTimeOffset now = DateTimeOffset.UtcNow; -#if NET8_0_OR_GREATER - _timeProvider.SetUtcNow(now); -#else - IDisposable replacement = Mock.Property(() => ClockResolver.UtcNowFunc, () => now); -#endif - await _deleteServiceWithExternalStore.DeleteInstanceAsync(studyInstanceUid, seriesInstanceUid, sopInstanceUid, CancellationToken.None); - await _indexDataStore - .Received(1) - .DeleteInstanceIndexAsync(Partition.Default, studyInstanceUid, seriesInstanceUid, sopInstanceUid, now); - } - - [Fact] - public async Task GivenADeleteInstanceRequestWithNonDefaultPartition_WhenDataStoreIsCalled_ThenNonDefaultPartitionIsUsed() - { - List responseList = GeneratedDeletedInstanceList(1, partition: new Partition(123, "ANonDefaultName")); - - _indexDataStore - .RetrieveDeletedInstancesWithPropertiesAsync(Arg.Any(), Arg.Any(), Arg.Any()) - .ReturnsForAnyArgs(responseList); - - (bool success, int retrievedInstanceCount) = await _deleteService.CleanupDeletedInstancesAsync(CancellationToken.None); - - await ValidateSuccessfulCleanupDeletedInstanceCall(success, responseList.Select(x => x.VersionedInstanceIdentifier).ToList(), retrievedInstanceCount); - } - - [Fact] - public async Task GivenNoDeletedInstances_WhenCleanupCalled_ThenNotCallStoresAndReturnsCorrectTuple() - { - _indexDataStore - .RetrieveDeletedInstancesWithPropertiesAsync(Arg.Any(), Arg.Any(), Arg.Any()) - .ReturnsForAnyArgs(new List()); - - (bool success, int retrievedInstanceCount) = await _deleteService.CleanupDeletedInstancesAsync(CancellationToken.None); - - Assert.True(success); - Assert.Equal(0, retrievedInstanceCount); - - await _indexDataStore - .ReceivedWithAnyArgs(1) - .RetrieveDeletedInstancesWithPropertiesAsync(batchSize: default, maxRetries: default, CancellationToken.None); - - await _indexDataStore - .DidNotReceiveWithAnyArgs() - .DeleteDeletedInstanceAsync(versionedInstanceIdentifier: default, CancellationToken.None); - - await _indexDataStore - .DidNotReceiveWithAnyArgs() - .IncrementDeletedInstanceRetryAsync(versionedInstanceIdentifier: default, cleanupAfter: default, CancellationToken.None); - - await _fileDataStore - .DidNotReceiveWithAnyArgs() - .DeleteFileIfExistsAsync(version: default, Partition.Default, _defaultFileProperties, CancellationToken.None); - - await _metadataStore - .DidNotReceiveWithAnyArgs() - .DeleteInstanceMetadataIfExistsAsync(version: default, CancellationToken.None); - - _transactionScope.Received(1).Complete(); - } - - [Fact] - public async Task GivenADeletedInstance_WhenFileStoreThrows_ThenIncrementRetryIsCalled() - { - DateTimeOffset now = DateTimeOffset.UtcNow; -#if NET8_0_OR_GREATER - _timeProvider.SetUtcNow(now); -#else - IDisposable replacement = Mock.Property(() => ClockResolver.UtcNowFunc, () => now); -#endif - List responseList = GeneratedDeletedInstanceList(1); - - _indexDataStore - .RetrieveDeletedInstancesWithPropertiesAsync(Arg.Any(), Arg.Any(), Arg.Any()) - .ReturnsForAnyArgs(responseList); - - _fileDataStore - .DeleteFileIfExistsAsync(Arg.Any(), Partition.Default, _defaultFileProperties, Arg.Any()) - .ThrowsForAnyArgs(new Exception("Generic exception")); - - (bool success, int retrievedInstanceCount) = await _deleteService.CleanupDeletedInstancesAsync(CancellationToken.None); - - Assert.True(success); - Assert.Equal(1, retrievedInstanceCount); - - await _indexDataStore - .Received(1) - .IncrementDeletedInstanceRetryAsync(responseList[0].VersionedInstanceIdentifier, now + _deleteConfiguration.RetryBackOff, CancellationToken.None); - } - - [Fact] - public async Task GivenADeletedInstanceWithExternalStore_WhenFileStoreThrows_ThenIncrementRetryIsCalled() - { - DateTimeOffset now = DateTimeOffset.UtcNow; -#if NET8_0_OR_GREATER - _timeProvider.SetUtcNow(now); -#else - IDisposable replacement = Mock.Property(() => ClockResolver.UtcNowFunc, () => now); -#endif - - List responseList = GeneratedDeletedInstanceList(1); - - _indexDataStore - .RetrieveDeletedInstancesWithPropertiesAsync(Arg.Any(), Arg.Any(), Arg.Any()) - .ReturnsForAnyArgs(responseList); - - _fileDataStore - .DeleteFileIfExistsAsync(Arg.Any(), Partition.Default, _defaultFileProperties, Arg.Any()) - .ThrowsForAnyArgs(new Exception("Generic exception")); - - (bool success, int retrievedInstanceCount) = await _deleteServiceWithExternalStore.CleanupDeletedInstancesAsync(CancellationToken.None); - - Assert.True(success); - Assert.Equal(1, retrievedInstanceCount); - - await _indexDataStore - .Received(1) - .IncrementDeletedInstanceRetryAsync(responseList[0].VersionedInstanceIdentifier, now + _deleteConfiguration.RetryBackOff, CancellationToken.None); - } - - [Fact] - public async Task GivenADeletedInstance_WhenMetadataStoreThrowsUnhandled_ThenIncrementRetryIsCalled() - { - List responseList = GeneratedDeletedInstanceList(1); - - _indexDataStore - .RetrieveDeletedInstancesWithPropertiesAsync(Arg.Any(), Arg.Any(), Arg.Any()) - .ReturnsForAnyArgs(responseList); - - _metadataStore - .DeleteInstanceMetadataIfExistsAsync(Arg.Any(), Arg.Any()) - .ThrowsForAnyArgs(new Exception("Generic exception")); - - (bool success, int retrievedInstanceCount) = await _deleteService.CleanupDeletedInstancesAsync(CancellationToken.None); - - Assert.True(success); - Assert.Equal(1, retrievedInstanceCount); - - await _indexDataStore - .Received(1) - .IncrementDeletedInstanceRetryAsync(responseList[0].VersionedInstanceIdentifier, cleanupAfter: Arg.Any(), CancellationToken.None); - } - - [Fact] - public async Task GivenADeletedInstance_WhenIncrementThrows_ThenSuccessIsReturnedFalse() - { - List responseList = GeneratedDeletedInstanceList(1); - - _indexDataStore - .RetrieveDeletedInstancesWithPropertiesAsync(Arg.Any(), Arg.Any(), Arg.Any()) - .ReturnsForAnyArgs(responseList); - - _fileDataStore - .DeleteFileIfExistsAsync(Arg.Any(), Partition.Default, _defaultFileProperties, Arg.Any()) - .ThrowsForAnyArgs(new Exception("Generic exception")); - - _indexDataStore - .IncrementDeletedInstanceRetryAsync(Arg.Any(), Arg.Any(), Arg.Any()) - .ThrowsForAnyArgs(new Exception("Generic exception")); - - (bool success, int retrievedInstanceCount) = await _deleteService.CleanupDeletedInstancesAsync(CancellationToken.None); - - Assert.False(success); - Assert.Equal(1, retrievedInstanceCount); - - await _indexDataStore - .Received(1) - .IncrementDeletedInstanceRetryAsync(responseList[0].VersionedInstanceIdentifier, cleanupAfter: Arg.Any(), CancellationToken.None); - } - - [Fact] - public async Task GivenADeletedInstance_WhenRetrieveThrows_ThenSuccessIsReturnedFalse() - { - _indexDataStore - .RetrieveDeletedInstancesWithPropertiesAsync(Arg.Any(), Arg.Any(), Arg.Any()) - .ThrowsForAnyArgs(new Exception("Generic exception")); - - (bool success, int retrievedInstanceCount) = await _deleteService.CleanupDeletedInstancesAsync(CancellationToken.None); - - Assert.False(success); - Assert.Equal(0, retrievedInstanceCount); - - await _indexDataStore - .DidNotReceiveWithAnyArgs() - .DeleteDeletedInstanceAsync(versionedInstanceIdentifier: default, CancellationToken.None); - - await _indexDataStore - .DidNotReceiveWithAnyArgs() - .IncrementDeletedInstanceRetryAsync(versionedInstanceIdentifier: default, cleanupAfter: default, CancellationToken.None); - - await _fileDataStore - .DidNotReceiveWithAnyArgs() - .DeleteFileIfExistsAsync(version: default, Partition.Default, _defaultFileProperties, CancellationToken.None); - } - - [Theory] - [InlineData(1)] - [InlineData(3)] - public async Task GivenMultipleDeletedInstance_WhenCleanupCalled_ThenCorrectMethodsAreCalledAndReturnsCorrectTuple(int numberOfDeletedInstances) - { - List responseList = GeneratedDeletedInstanceList(numberOfDeletedInstances); - - _indexDataStore - .RetrieveDeletedInstancesWithPropertiesAsync(Arg.Any(), Arg.Any(), Arg.Any()) - .ReturnsForAnyArgs(responseList); - - (bool success, int retrievedInstanceCount) = await _deleteService.CleanupDeletedInstancesAsync(CancellationToken.None); - - await ValidateSuccessfulCleanupDeletedInstanceCall(success, responseList.Select(x => x.VersionedInstanceIdentifier).ToList(), retrievedInstanceCount); - } - - [Fact] - public async Task GivenMultipleDeletedInstancePreviouslyUpdatedAndNowWithOriginalWatermark_WhenCleanupCalledWithoutExternalStore_ThenMethodsAreCalledWhileUsingOriginalWatermark() - { - List responseList = - GeneratedDeletedInstanceList( - 2, - new InstanceProperties { OriginalVersion = 1, NewVersion = null }, - generateUniqueFileProperties: false).Take(1).ToList(); - - // ensure no instances contain file properties - Assert.DoesNotContain(responseList, i => i.InstanceProperties.FileProperties != null); - - _indexDataStore - .RetrieveDeletedInstancesWithPropertiesAsync(Arg.Any(), Arg.Any(), Arg.Any()) - .ReturnsForAnyArgs(responseList); - - (bool success, int retrievedInstanceCount) = await _deleteService.CleanupDeletedInstancesAsync(CancellationToken.None); - - Assert.True(success); - Assert.Equal(responseList.Count, retrievedInstanceCount); - - await _indexDataStore - .ReceivedWithAnyArgs(1) - .RetrieveDeletedInstancesWithPropertiesAsync(default, default, CancellationToken.None); - - foreach (InstanceMetadata instance in responseList) - { - var deletedVersion = instance.VersionedInstanceIdentifier; - await _indexDataStore - .Received(1) - .DeleteDeletedInstanceAsync(deletedVersion, CancellationToken.None); - - // delete both original and new version's metadata - await _metadataStore - .Received(1) - .DeleteInstanceMetadataIfExistsAsync(deletedVersion.Version, CancellationToken.None); - await _metadataStore - .Received(1) - .DeleteInstanceMetadataIfExistsAsync(instance.InstanceProperties.OriginalVersion.Value, CancellationToken.None); - - Assert.Null(instance.InstanceProperties.FileProperties); - - // delete both original and new version's blobs and fileProperties are null and not used - await _fileDataStore - .Received(1) - .DeleteFileIfExistsAsync(deletedVersion.Version, deletedVersion.Partition, fileProperties: null, cancellationToken: CancellationToken.None); - - await _fileDataStore - .Received(1) - .DeleteFileIfExistsAsync(instance.InstanceProperties.OriginalVersion.Value, deletedVersion.Partition, fileProperties: null, CancellationToken.None); - } - - await _indexDataStore - .DidNotReceiveWithAnyArgs() - .IncrementDeletedInstanceRetryAsync(versionedInstanceIdentifier: default, cleanupAfter: default, CancellationToken.None); - - _transactionScope.Received(1).Complete(); - } - - [Fact] - public async Task GivenMultipleDeletedInstanceWithExternalStore_WhenCleanupCalled_ThenMethodsAreCalledWithNonNullFileProperties() - { - List responseList = - GeneratedDeletedInstanceList(2, generateUniqueFileProperties: true); - - // ensure instances contain file properties that are not null - Assert.DoesNotContain(responseList, i => i.InstanceProperties.FileProperties == null); - - _indexDataStore - .RetrieveDeletedInstancesWithPropertiesAsync(Arg.Any(), Arg.Any(), Arg.Any()) - .ReturnsForAnyArgs(responseList); - - (bool success, int retrievedInstanceCount) = await _deleteServiceWithExternalStore.CleanupDeletedInstancesAsync(CancellationToken.None); - - Assert.True(success); - Assert.Equal(responseList.Count, retrievedInstanceCount); - - await _indexDataStore - .ReceivedWithAnyArgs(1) - .RetrieveDeletedInstancesWithPropertiesAsync(default, default, CancellationToken.None); - - foreach (InstanceMetadata instance in responseList) - { - var deletedVersion = instance.VersionedInstanceIdentifier; - await _indexDataStore - .Received(1) - .DeleteDeletedInstanceAsync(deletedVersion, CancellationToken.None); - - await _metadataStore - .Received(1) - .DeleteInstanceMetadataIfExistsAsync(deletedVersion.Version, CancellationToken.None); - - Assert.NotNull(instance.InstanceProperties.FileProperties); - - await _fileDataStore - .Received(1) - .DeleteFileIfExistsAsync(deletedVersion.Version, deletedVersion.Partition, instance.InstanceProperties.FileProperties, CancellationToken.None); - } - - await _indexDataStore - .DidNotReceiveWithAnyArgs() - .IncrementDeletedInstanceRetryAsync(versionedInstanceIdentifier: default, cleanupAfter: default, CancellationToken.None); - - _transactionScope.Received(1).Complete(); - } - - [Fact] - public async Task GivenAvailableDatabase_WhenFetchingMetrics_ThenSuccessfullyFetch() - { - const int ExhaustedRetries = 42; - DateTimeOffset oldestTimestamp = DateTimeOffset.UtcNow.AddMonths(-1); - - using CancellationTokenSource source = new(); - - _indexDataStore.GetOldestDeletedAsync(source.Token).Returns(oldestTimestamp); - _indexDataStore - .RetrieveNumExhaustedDeletedInstanceAttemptsAsync(_deleteConfiguration.MaxRetries, source.Token) - .Returns(ExhaustedRetries); - - DeleteMetrics actual = await _deleteService.GetMetricsAsync(source.Token); - - Assert.Equal(oldestTimestamp, actual.OldestDeletion); - Assert.Equal(ExhaustedRetries, actual.TotalExhaustedRetries); - } - - private async Task ValidateSuccessfulCleanupDeletedInstanceCall(bool success, IReadOnlyCollection responseList, int retrievedInstanceCount, FileProperties expectedFileProperties = null) - { - Assert.True(success); - Assert.Equal(responseList.Count, retrievedInstanceCount); - - await _indexDataStore - .ReceivedWithAnyArgs(1) - .RetrieveDeletedInstancesWithPropertiesAsync(default, default, CancellationToken.None); - - foreach (VersionedInstanceIdentifier deletedVersion in responseList) - { - await _indexDataStore - .Received(1) - .DeleteDeletedInstanceAsync(deletedVersion, CancellationToken.None); - - await _metadataStore - .Received(1) - .DeleteInstanceMetadataIfExistsAsync(deletedVersion.Version, CancellationToken.None); - - await _fileDataStore - .Received(1) - .DeleteFileIfExistsAsync(deletedVersion.Version, deletedVersion.Partition, expectedFileProperties, CancellationToken.None); - } - - await _indexDataStore - .DidNotReceiveWithAnyArgs() - .IncrementDeletedInstanceRetryAsync(versionedInstanceIdentifier: default, cleanupAfter: default, CancellationToken.None); - - _transactionScope.Received(1).Complete(); - } - - private static List GeneratedDeletedInstanceList(int numberOfResults, InstanceProperties instanceProperties = null, Partition partition = null, FileProperties fileProperties = null, bool generateUniqueFileProperties = false) - { - if (generateUniqueFileProperties) - { - fileProperties = new FileProperties - { - Path = Guid.NewGuid() + ".dcm", - ETag = "e" + Guid.NewGuid(), - ContentLength = 123 - }; - } - instanceProperties ??= new InstanceProperties() { FileProperties = fileProperties }; - partition ??= Partition.Default; - var deletedInstanceList = new List(); - for (int i = 0; i < numberOfResults; i++) - { - string studyInstanceUid = TestUidGenerator.Generate(); - string seriesInstanceUid = TestUidGenerator.Generate(); - string sopInstanceUid = TestUidGenerator.Generate(); - deletedInstanceList.Add(new InstanceMetadata(new VersionedInstanceIdentifier(studyInstanceUid, seriesInstanceUid, sopInstanceUid, i, partition), instanceProperties)); - } - - return deletedInstanceList; - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Diagnostic/LogForwarderExtensionsTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Diagnostic/LogForwarderExtensionsTests.cs deleted file mode 100644 index 2c30829416..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Diagnostic/LogForwarderExtensionsTests.cs +++ /dev/null @@ -1,181 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using Microsoft.ApplicationInsights; -using Microsoft.ApplicationInsights.Extensibility; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Diagnostic; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Tests.Common; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Diagnostic; - -public class LogForwarderExtensionsTests -{ - [Fact] - public void GivenClientUsingForwardTelemetry_ExpectForwardLogFlagIsSetWithNoAdditionalProperties() - { - (TelemetryClient telemetryClient, var channel) = CreateTelemetryClientWithChannel(); - telemetryClient.ForwardLogTrace("A message"); - - Assert.Single(channel.Items); -#pragma warning disable CS0618 // Type or member is obsolete - Assert.Single(channel.Items[0].Context.Properties); - Assert.Equal(Boolean.TrueString, channel.Items[0].Context.Properties["forwardLog"]); -#pragma warning restore CS0618 // Type or member is obsolete - } - - [Fact] - public void GivenClientUsingForwardTelemetryWithFileProperties_ExpectForwardLogFlagIsSetWithAdditionalProperties() - { - (TelemetryClient telemetryClient, var channel) = CreateTelemetryClientWithChannel(); - var expectedProperties = new FileProperties { Path = "123.dcm", ETag = "e456", ContentLength = 123 }; - var expectedPartition = new Partition(1, "partitionOne"); - telemetryClient.ForwardLogTrace("A message", expectedPartition, expectedProperties); - - Assert.Single(channel.Items); -#pragma warning disable CS0618 // Type or member is obsolete - Assert.Equal(4, channel.Items[0].Context.Properties.Count); - Assert.Equal(Boolean.TrueString, channel.Items[0].Context.Properties["forwardLog"]); - Assert.Equal(expectedProperties.Path, channel.Items[0].Context.Properties["dicomAdditionalInformation_filePropertiesPath"]); - Assert.Equal(expectedProperties.ETag, channel.Items[0].Context.Properties["dicomAdditionalInformation_filePropertiesETag"]); - Assert.Equal(expectedPartition.Name, channel.Items[0].Context.Properties["dicomAdditionalInformation_partitionName"]); -#pragma warning restore CS0618 // Type or member is obsolete - } - - [Fact] - public void GivenClientUsingForwardTelemetryWithFileProperties_ExpectForwardLogFlagIsSetWithAdditionalPropertiesWithoutPartition() - { - (TelemetryClient telemetryClient, var channel) = CreateTelemetryClientWithChannel(); - var expectedProperties = new FileProperties { Path = "123.dcm", ETag = "e456", ContentLength = 123 }; - - // because a default partition is being used, we don't log it in telemetry - var expectedPartition = Partition.Default; - telemetryClient.ForwardLogTrace("A message", expectedPartition, expectedProperties); - - Assert.Single(channel.Items); -#pragma warning disable CS0618 // Type or member is obsolete - Assert.Equal(3, channel.Items[0].Context.Properties.Count); - Assert.Equal(Boolean.TrueString, channel.Items[0].Context.Properties["forwardLog"]); - Assert.Equal(expectedProperties.Path, channel.Items[0].Context.Properties["dicomAdditionalInformation_filePropertiesPath"]); - Assert.Equal(expectedProperties.ETag, channel.Items[0].Context.Properties["dicomAdditionalInformation_filePropertiesETag"]); -#pragma warning restore CS0618 // Type or member is obsolete - } - - [Fact] - public void GivenClientUsingForwardTelemetryWithIdentifier_ExpectForwardLogFlagIsSetWithAdditionalProperties() - { - (TelemetryClient telemetryClient, var channel) = CreateTelemetryClientWithChannel(); - - var expectedIdentifier = new InstanceIdentifier(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), new Partition(1, "partitionOne")); - telemetryClient.ForwardLogTrace("A message", expectedIdentifier); - - Assert.Single(channel.Items); -#pragma warning disable CS0618 // Type or member is obsolete - Assert.Equal(5, channel.Items[0].Context.Properties.Count); - Assert.Equal(expectedIdentifier.SopInstanceUid, - channel.Items[0].Context.Properties["dicomAdditionalInformation_sopInstanceUID"]); - Assert.Equal(expectedIdentifier.SeriesInstanceUid, - channel.Items[0].Context.Properties["dicomAdditionalInformation_seriesInstanceUID"]); - Assert.Equal(expectedIdentifier.StudyInstanceUid, - channel.Items[0].Context.Properties["dicomAdditionalInformation_studyInstanceUID"]); - Assert.Equal(expectedIdentifier.Partition.Name, - channel.Items[0].Context.Properties["dicomAdditionalInformation_partitionName"]); - Assert.Equal(Boolean.TrueString, channel.Items[0].Context.Properties["forwardLog"]); -#pragma warning restore CS0618 // Type or member is obsolete - } - - [Fact] - public void GivenClientUsingForwardTelemetryWithIdentifier_ExpectForwardLogFlagIsSetWithAdditionalPropertiesWithoutPartition() - { - (TelemetryClient telemetryClient, var channel) = CreateTelemetryClientWithChannel(); - - // because a default partition is being used, we don't log it in telemetry - var expectedIdentifier = new InstanceIdentifier(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), Partition.Default); - telemetryClient.ForwardLogTrace("A message", expectedIdentifier); - - Assert.Single(channel.Items); -#pragma warning disable CS0618 // Type or member is obsolete - Assert.Equal(4, channel.Items[0].Context.Properties.Count); - Assert.Equal(expectedIdentifier.SopInstanceUid, - channel.Items[0].Context.Properties["dicomAdditionalInformation_sopInstanceUID"]); - Assert.Equal(expectedIdentifier.SeriesInstanceUid, - channel.Items[0].Context.Properties["dicomAdditionalInformation_seriesInstanceUID"]); - Assert.Equal(expectedIdentifier.StudyInstanceUid, - channel.Items[0].Context.Properties["dicomAdditionalInformation_studyInstanceUID"]); - Assert.Equal(Boolean.TrueString, channel.Items[0].Context.Properties["forwardLog"]); -#pragma warning restore CS0618 // Type or member is obsolete - } - - [Fact] - public void GivenClientUsingForwardTelemetry_whenForwardOperationLogTraceWithSizeLimit_ExpectForwardLogFlagIsSet() - { - (TelemetryClient telemetryClient, var channel) = CreateTelemetryClientWithChannel(); - - var operationId = Guid.NewGuid().ToString(); - var input = "input"; - var message = "a message"; - telemetryClient.ForwardOperationLogTrace(message, operationId, input, "update"); - - Assert.Single(channel.Items); -#pragma warning disable CS0618 // Type or member is obsolete - Assert.Equal(4, channel.Items[0].Context.Properties.Count); - Assert.Equal(Boolean.TrueString, channel.Items[0].Context.Properties["forwardLog"]); - Assert.Equal(operationId, channel.Items[0].Context.Properties["dicomAdditionalInformation_operationId"]); - Assert.Equal(input, channel.Items[0].Context.Properties["dicomAdditionalInformation_input"]); - Assert.Equal("update", channel.Items[0].Context.Properties["operationName"]); -#pragma warning restore CS0618 // Type or member is obsolete - } - - [Fact] - public void GivenClientUsingForwardTelemetry_whenForwardOperationLogTraceWithSizeLimitExceeded_ExpectForwardLogFlagIsSetAndMultipleTelemetriesEmitted() - { - (TelemetryClient telemetryClient, var channel) = CreateTelemetryClientWithChannel(); - - var operationId = Guid.NewGuid().ToString(); - var expectedFirstItemInput = "a".PadRight(32 * 1024); // split occurs at 32 kb - var expectedSecondItemInput = "b".PadRight(32 * 1024); // split occurs at 32 kb - var fullInput = expectedFirstItemInput + expectedSecondItemInput; - var message = "a message"; - telemetryClient.ForwardOperationLogTrace(message, operationId, fullInput, "update"); - - Assert.Equal(2, channel.Items.Count); - -#pragma warning disable CS0618 // Type or member is obsolete - var firstItem = channel.Items[0]; - Assert.Equal(4, firstItem.Context.Properties.Count); - Assert.Equal(Boolean.TrueString, firstItem.Context.Properties["forwardLog"]); - Assert.Equal(operationId, firstItem.Context.Properties["dicomAdditionalInformation_operationId"]); - Assert.Equal(expectedFirstItemInput, firstItem.Context.Properties["dicomAdditionalInformation_input"]); - Assert.Equal("update", firstItem.Context.Properties["operationName"]); - - var secondItem = channel.Items[1]; - Assert.Equal(4, secondItem.Context.Properties.Count); - Assert.Equal(Boolean.TrueString, secondItem.Context.Properties["forwardLog"]); - Assert.Equal(operationId, secondItem.Context.Properties["dicomAdditionalInformation_operationId"]); - Assert.Equal(expectedSecondItemInput, secondItem.Context.Properties["dicomAdditionalInformation_input"]); - Assert.Equal("update", firstItem.Context.Properties["operationName"]); -#pragma warning restore CS0618 // Type or member is obsolete - } - - private static (TelemetryClient, MockTelemetryChannel) CreateTelemetryClientWithChannel() - { - MockTelemetryChannel channel = new MockTelemetryChannel(); - - TelemetryConfiguration configuration = new TelemetryConfiguration - { - TelemetryChannel = channel, -#pragma warning disable CS0618 // Type or member is obsolete - InstrumentationKey = Guid.NewGuid().ToString() -#pragma warning restore CS0618 // Type or member is obsolete - }; - configuration.TelemetryInitializers.Add(new OperationCorrelationTelemetryInitializer()); - - return (new TelemetryClient(configuration), channel); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Diagnostic/MockTelemetryChannel.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Diagnostic/MockTelemetryChannel.cs deleted file mode 100644 index 01f239a027..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Diagnostic/MockTelemetryChannel.cs +++ /dev/null @@ -1,33 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using Microsoft.ApplicationInsights.Channel; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Diagnostic; - -internal class MockTelemetryChannel : ITelemetryChannel -{ - public IList Items { get; private set; } = new List(); - - public void Send(ITelemetry item) - { - Items.Add(item); - } - - public void Flush() - { - throw new NotImplementedException(); - } - - public bool? DeveloperMode { get; set; } - public string EndpointAddress { get; set; } - - public void Dispose() - { - throw new NotImplementedException(); - } -} \ No newline at end of file diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Export/ExportHandlerTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Export/ExportHandlerTests.cs deleted file mode 100644 index 0ac65124bc..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Export/ExportHandlerTests.cs +++ /dev/null @@ -1,81 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Health.Core.Features.Security.Authorization; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Export; -using Microsoft.Health.Dicom.Core.Features.Security; -using Microsoft.Health.Dicom.Core.Messages.Export; -using Microsoft.Health.Dicom.Core.Models.Export; -using Microsoft.Health.Operations; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Export; - -public class ExportHandlerTests -{ - private readonly IAuthorizationService _auth; - private readonly IExportService _export; - private readonly ExportHandler _handler; - - public ExportHandlerTests() - { - _auth = Substitute.For>(); - _export = Substitute.For(); - _handler = new ExportHandler(_auth, _export); - } - - [Fact] - public void GivenNullArgument_WhenConstructing_ThenThrowArgumentNullException() - { - Assert.Throws(() => new ExportHandler(null, _export)); - Assert.Throws(() => new ExportHandler(_auth, null)); - } - - [Fact] - public async Task GivenNullRequest_WhenHandlingRequest_ThenThrowArgumentNullException() - { - await Assert.ThrowsAsync(() => _handler.Handle(null, default)); - - await _auth.DidNotReceiveWithAnyArgs().CheckAccess(default, default); - await _export.DidNotReceiveWithAnyArgs().StartExportAsync(default, default); - } - - [Fact] - public async Task GivenNoAccess_WhenHandlingRequest_ThenThrowUnauthorizedDicomActionException() - { - using var tokenSource = new CancellationTokenSource(); - - _auth.CheckAccess(DataActions.Export, tokenSource.Token).Returns(DataActions.None); - await Assert.ThrowsAsync( - () => _handler.Handle(new ExportRequest(new ExportSpecification()), tokenSource.Token)); - - await _auth.Received(1).CheckAccess(DataActions.Export, tokenSource.Token); - await _export.DidNotReceiveWithAnyArgs().StartExportAsync(default, default); - } - - [Fact] - public async Task GivenRequest_WhenHandlingRequest_ThenReturnResponse() - { - using var tokenSource = new CancellationTokenSource(); - var request = new ExportRequest(new ExportSpecification()); - var expected = new OperationReference(Guid.NewGuid(), new Uri("http://operation")); - - _auth.CheckAccess(DataActions.Export, tokenSource.Token).Returns(DataActions.Export); - _export.StartExportAsync(request.Specification, tokenSource.Token).Returns(expected); - - ExportResponse response = await _handler.Handle(request, tokenSource.Token); - Assert.Same(expected, response.Operation); - - await _auth.Received(1).CheckAccess(DataActions.Export, tokenSource.Token); - await _export - .Received(1) - .StartExportAsync(request.Specification, tokenSource.Token); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Export/ExportServiceTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Export/ExportServiceTests.cs deleted file mode 100644 index 892ff4e08d..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Export/ExportServiceTests.cs +++ /dev/null @@ -1,128 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Context; -using Microsoft.Health.Dicom.Core.Features.Export; -using Microsoft.Health.Dicom.Core.Features.Operations; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Core.Models.Export; -using Microsoft.Health.Operations; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Export; - -public class ExportServiceTests -{ - private const ExportSourceType SourceType = ExportSourceType.Identifiers; - private const ExportDestinationType DestinationType = ExportDestinationType.AzureBlob; - - private readonly Partition _partition = new Partition(123, "export-partition"); - private readonly IExportSink _sink; - private readonly IExportSourceProvider _sourceProvider; - private readonly IExportSinkProvider _sinkProvider; - private readonly IGuidFactory _guidFactory; - private readonly IDicomOperationsClient _client; - private readonly IDicomRequestContextAccessor _requestContextAccessor; - private readonly IDicomRequestContext _requestContext; - private readonly ExportService _service; - - public ExportServiceTests() - { - _sourceProvider = Substitute.For(); - _sourceProvider.Type.Returns(SourceType); - _sink = Substitute.For(); - _sinkProvider = Substitute.For(); - _sinkProvider.Type.Returns(DestinationType); - _client = Substitute.For(); - _guidFactory = Substitute.For(); - _requestContextAccessor = Substitute.For(); - _requestContext = Substitute.For(); - _requestContext.DataPartition.Returns(_partition); - _requestContextAccessor.RequestContext.Returns(_requestContext); - _service = new ExportService( - new ExportSourceFactory(new IExportSourceProvider[] { _sourceProvider }), - new ExportSinkFactory(new IExportSinkProvider[] { _sinkProvider }), - _guidFactory, - _client, - _requestContextAccessor); - } - - [Fact] - public void GivenNullArgument_WhenConstructing_ThenThrowArgumentNullException() - { - var source = new ExportSourceFactory(new IExportSourceProvider[] { _sourceProvider }); - var sink = new ExportSinkFactory(new IExportSinkProvider[] { _sinkProvider }); - - Assert.Throws(() => new ExportService(null, sink, _guidFactory, _client, _requestContextAccessor)); - Assert.Throws(() => new ExportService(source, null, _guidFactory, _client, _requestContextAccessor)); - Assert.Throws(() => new ExportService(source, sink, null, _client, _requestContextAccessor)); - Assert.Throws(() => new ExportService(source, sink, _guidFactory, null, _requestContextAccessor)); - Assert.Throws(() => new ExportService(source, sink, _guidFactory, _client, null)); - } - - [Fact] - public async Task GivenSpecification_WhenStartingExport_ThenValidateBeforeStarting() - { - using var tokenSource = new CancellationTokenSource(); - - var operationId = Guid.NewGuid(); - var sourceSettings = new object(); - var originalDestinationSettings = new object(); - var securedDestinationSettings = new object(); - var spec = new ExportSpecification - { - Destination = new ExportDataOptions(DestinationType, originalDestinationSettings), - Source = new ExportDataOptions(SourceType, sourceSettings), - }; - var errorHref = new Uri($"https://somewhere/{operationId:N}/errors.log"); - var expected = new OperationReference(operationId, new Uri("http://test/export")); - - _guidFactory.Create().Returns(operationId); - _sinkProvider.CreateAsync(originalDestinationSettings, operationId, tokenSource.Token).Returns(_sink); - _sinkProvider.SecureSensitiveInfoAsync(originalDestinationSettings, operationId, tokenSource.Token).Returns(securedDestinationSettings); - _sink.InitializeAsync(tokenSource.Token).Returns(errorHref); - _client - .StartExportAsync( - operationId, - Arg.Is(x => ReferenceEquals(sourceSettings, x.Source.Settings) - && ReferenceEquals(securedDestinationSettings, x.Destination.Settings)), - errorHref, - _partition, - tokenSource.Token) - .Returns(expected); - - Assert.Same(expected, await _service.StartExportAsync(spec, tokenSource.Token)); - - _guidFactory.Received(1).Create(); - await _sourceProvider.Received(1).ValidateAsync(sourceSettings, tokenSource.Token); - await _sinkProvider.Received(1).ValidateAsync(originalDestinationSettings, tokenSource.Token); - await _sinkProvider.Received(1).CreateAsync(originalDestinationSettings, operationId, tokenSource.Token); - await _sinkProvider.Received(1).SecureSensitiveInfoAsync(originalDestinationSettings, operationId, tokenSource.Token); - await _sink.Received(1).InitializeAsync(tokenSource.Token); - await _client - .Received(1) - .StartExportAsync( - operationId, - Arg.Is(x => ReferenceEquals(sourceSettings, x.Source.Settings) - && ReferenceEquals(securedDestinationSettings, x.Destination.Settings)), - errorHref, - _partition, - tokenSource.Token); - - // Ensure that validation was called before creation - Received.InOrder( - () => - { - _sourceProvider.ValidateAsync(sourceSettings, tokenSource.Token); - _sinkProvider.ValidateAsync(originalDestinationSettings, tokenSource.Token); - _sinkProvider.CreateAsync(originalDestinationSettings, operationId, tokenSource.Token); - }); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Export/ExportSinkFactoryTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Export/ExportSinkFactoryTests.cs deleted file mode 100644 index eac0109132..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Export/ExportSinkFactoryTests.cs +++ /dev/null @@ -1,128 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Health.Dicom.Core.Features.Export; -using Microsoft.Health.Dicom.Core.Models.Export; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Export; - -public class ExportSinkFactoryTests -{ - [Fact] - public async Task GivenNoProviders_WhenCompletingCopy_ThenThrowException() - { - var factory = new ExportSinkFactory(Array.Empty()); - await Assert.ThrowsAsync( - () => factory.CompleteCopyAsync(new ExportDataOptions(ExportDestinationType.AzureBlob))); - } - - [Fact] - public async Task GivenValidProviders_WhenCompletingCopy_ThenInvokeCorrectMethod() - { - using var tokenSource = new CancellationTokenSource(); - - var options = new AzureBlobExportOptions(); - var destination = new ExportDataOptions(ExportDestinationType.AzureBlob, options); - - IExportSinkProvider provider = Substitute.For(); - provider.Type.Returns(ExportDestinationType.AzureBlob); - - var factory = new ExportSinkFactory(new IExportSinkProvider[] { provider }); - await factory.CompleteCopyAsync(destination, tokenSource.Token); - - await provider.Received(1).CompleteCopyAsync(options, tokenSource.Token); - } - - [Fact] - public async Task GivenNoProviders_WhenCreatingSink_ThenThrowException() - { - var factory = new ExportSinkFactory(Array.Empty()); - await Assert.ThrowsAsync(() => factory.CreateAsync( - new ExportDataOptions(ExportDestinationType.AzureBlob), - Guid.NewGuid())); - } - - [Fact] - public async Task GivenValidProviders_WhenCreatingSink_ThenReturnSink() - { - using var tokenSource = new CancellationTokenSource(); - - var options = new AzureBlobExportOptions(); - var destination = new ExportDataOptions(ExportDestinationType.AzureBlob, options); - var operationId = Guid.NewGuid(); - IExportSink expected = Substitute.For(); - - IExportSinkProvider provider = Substitute.For(); - provider.Type.Returns(ExportDestinationType.AzureBlob); - provider.CreateAsync(options, operationId, tokenSource.Token).Returns(expected); - - var factory = new ExportSinkFactory(new IExportSinkProvider[] { provider }); - Assert.Same(expected, await factory.CreateAsync(destination, operationId, tokenSource.Token)); - - await provider.Received(1).CreateAsync(options, operationId, tokenSource.Token); - } - - [Fact] - public async Task GivenNoProviders_WhenSecuring_ThenThrowException() - { - var factory = new ExportSinkFactory(Array.Empty()); - await Assert.ThrowsAsync(() => factory.SecureSensitiveInfoAsync( - new ExportDataOptions(ExportDestinationType.AzureBlob), - Guid.NewGuid())); - } - - [Fact] - public async Task GivenValidProviders_WhenSecuring_ThenInvokeCorrectMethod() - { - using var tokenSource = new CancellationTokenSource(); - - var operationId = Guid.NewGuid(); - var options = new AzureBlobExportOptions(); - var destination = new ExportDataOptions(ExportDestinationType.AzureBlob, options); - var expected = new AzureBlobExportOptions(); - - IExportSinkProvider provider = Substitute.For(); - provider.Type.Returns(ExportDestinationType.AzureBlob); - provider.SecureSensitiveInfoAsync(options, operationId, tokenSource.Token).Returns(expected); - - var factory = new ExportSinkFactory(new IExportSinkProvider[] { provider }); - ExportDataOptions actual = await factory.SecureSensitiveInfoAsync(destination, operationId, tokenSource.Token); - - await provider.Received(1).SecureSensitiveInfoAsync(options, operationId, tokenSource.Token); - Assert.Equal(ExportDestinationType.AzureBlob, actual.Type); - Assert.Same(expected, actual.Settings); - } - - [Fact] - public async Task GivenNoProviders_WhenValidating_ThenThrowException() - { - var factory = new ExportSinkFactory(Array.Empty()); - await Assert.ThrowsAsync( - () => factory.ValidateAsync(new ExportDataOptions(ExportDestinationType.AzureBlob))); - } - - [Fact] - public async Task GivenValidProviders_WhenValidating_ThenInvokeCorrectMethod() - { - using var tokenSource = new CancellationTokenSource(); - - var options = new AzureBlobExportOptions(); - var destination = new ExportDataOptions(ExportDestinationType.AzureBlob, options); - - IExportSinkProvider provider = Substitute.For(); - provider.Type.Returns(ExportDestinationType.AzureBlob); - - var factory = new ExportSinkFactory(new IExportSinkProvider[] { provider }); - await factory.ValidateAsync(destination, tokenSource.Token); - - await provider.Received(1).ValidateAsync(options, tokenSource.Token); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Export/ExportSourceFactoryTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Export/ExportSourceFactoryTests.cs deleted file mode 100644 index 78f28c916f..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Export/ExportSourceFactoryTests.cs +++ /dev/null @@ -1,72 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Health.Dicom.Core.Features.Export; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Core.Models.Export; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Export; - -public class ExportSourceFactoryTests -{ - [Fact] - public async Task GivenNoProviders_WhenCreatingSource_ThenThrowException() - { - var factory = new ExportSourceFactory(Array.Empty()); - await Assert.ThrowsAsync( - () => factory.CreateAsync(new ExportDataOptions(ExportSourceType.Identifiers), Partition.Default)); - } - - [Fact] - public async Task GivenValidProviders_WhenCreatingSource_ThenReturnSource() - { - using var tokenSource = new CancellationTokenSource(); - - var options = new IdentifierExportOptions(); - var partition = Partition.Default; - var source = new ExportDataOptions(ExportSourceType.Identifiers, options); - IExportSource expected = Substitute.For(); - - IExportSourceProvider provider = Substitute.For(); - provider.Type.Returns(ExportSourceType.Identifiers); - provider.CreateAsync(options, partition, tokenSource.Token).Returns(expected); - - var factory = new ExportSourceFactory(new IExportSourceProvider[] { provider }); - Assert.Same(expected, await factory.CreateAsync(source, partition, tokenSource.Token)); - - await provider.Received(1).CreateAsync(options, partition, tokenSource.Token); - } - - [Fact] - public async Task GivenNoProviders_WhenValidating_ThenThrowException() - { - var factory = new ExportSourceFactory(Array.Empty()); - await Assert.ThrowsAsync( - () => factory.ValidateAsync(new ExportDataOptions(ExportSourceType.Identifiers))); - } - - [Fact] - public async Task GivenValidProviders_WhenValidating_ThenInvokeCorrectMethod() - { - using var tokenSource = new CancellationTokenSource(); - - var options = new IdentifierExportOptions(); - var source = new ExportDataOptions(ExportSourceType.Identifiers, options); - - IExportSourceProvider provider = Substitute.For(); - provider.Type.Returns(ExportSourceType.Identifiers); - - var factory = new ExportSourceFactory(new IExportSourceProvider[] { provider }); - await factory.ValidateAsync(source, tokenSource.Token); - - await provider.Received(1).ValidateAsync(options, tokenSource.Token); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Export/IdentifierExportSourceProviderTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Export/IdentifierExportSourceProviderTests.cs deleted file mode 100644 index e8389d87da..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Export/IdentifierExportSourceProviderTests.cs +++ /dev/null @@ -1,56 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading.Tasks; -using Microsoft.Health.Dicom.Core.Features.Export; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Core.Features.Retrieve; -using Microsoft.Health.Dicom.Core.Models.Common; -using Microsoft.Health.Dicom.Core.Models.Export; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Export; - -public class IdentifierExportSourceProviderTests -{ - private readonly IdentifierExportSourceProvider _provider; - - public IdentifierExportSourceProviderTests() - => _provider = new IdentifierExportSourceProvider(Substitute.For()); - - [Fact] - public async Task GivenConfig_WhenCreatingSource_ThenReturnSource() - { - var options = new IdentifierExportOptions - { - Values = new DicomIdentifier[] - { - DicomIdentifier.ForInstance("1.2", "3.4.5", "6.7.8.10"), - DicomIdentifier.ForSeries("11.12.13", "14"), - DicomIdentifier.ForStudy("1516.17"), - }, - }; - - IExportSource source = await _provider.CreateAsync(options, Partition.Default); - Assert.IsType(source); - } - - [Fact] - public async Task GivenValidConfig_WhenValidating_ThenPass() - { - var options = new IdentifierExportOptions - { - Values = new DicomIdentifier[] - { - DicomIdentifier.ForInstance("1.2", "3.4.5", "6.7.8.10"), - DicomIdentifier.ForSeries("11.12.13", "14"), - DicomIdentifier.ForStudy("1516.17"), - }, - }; - - await _provider.ValidateAsync(options); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Export/IdentifierExportSourceTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Export/IdentifierExportSourceTests.cs deleted file mode 100644 index 982a0cba55..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Export/IdentifierExportSourceTests.cs +++ /dev/null @@ -1,170 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Health.Dicom.Core.Features.Export; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Core.Features.Retrieve; -using Microsoft.Health.Dicom.Core.Models.Common; -using Microsoft.Health.Dicom.Core.Models.Export; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Export; - -public class IdentifierExportSourceTests -{ - private readonly IInstanceStore _store; - private readonly Partition _partition; - private readonly IdentifierExportOptions _options; - - public IdentifierExportSourceTests() - { - _store = Substitute.For(); - _partition = new Partition(99, "test"); - _options = new IdentifierExportOptions(); - } - - [Fact] - public async Task GivenInstances_WhenEnumerating_ThenYieldValues() - { - // Configure input - _options.Values = new DicomIdentifier[] - { - DicomIdentifier.ForStudy("10"), - DicomIdentifier.ForStudy("11"), - DicomIdentifier.ForSeries("100", "200"), - DicomIdentifier.ForSeries("100", "201"), - DicomIdentifier.ForInstance("1000", "2000", "3000"), - DicomIdentifier.ForInstance("1000", "2000", "3001"), - }; - - using var tokenSource = new CancellationTokenSource(); - await using var source = new IdentifierExportSource(_store, _partition, _options); - - var expected = new[] - { - new InstanceMetadata(new VersionedInstanceIdentifier("10", "10", "10", 1, _partition), new InstanceProperties()), - new InstanceMetadata(new VersionedInstanceIdentifier("10", "10", "20", 3, _partition), new InstanceProperties()), - new InstanceMetadata(new VersionedInstanceIdentifier("10", "20", "10", 1, _partition), new InstanceProperties()), - new InstanceMetadata(new VersionedInstanceIdentifier("100", "200", "300", 2, _partition), new InstanceProperties()), - new InstanceMetadata(new VersionedInstanceIdentifier("100", "200", "400", 7, _partition), new InstanceProperties()), - new InstanceMetadata(new VersionedInstanceIdentifier("100", "200", "500", 2, _partition), new InstanceProperties()), - new InstanceMetadata(new VersionedInstanceIdentifier("1000", "2000", "3000", 1, _partition), new InstanceProperties()), - }; - _store - .GetInstanceIdentifierWithPropertiesAsync(_partition, "10", null, null, false, tokenSource.Token) - .Returns(expected[..3]); - _store - .GetInstanceIdentifierWithPropertiesAsync(_partition, "11", null, null, false, tokenSource.Token) - .Returns(Array.Empty()); - - _store - .GetInstanceIdentifierWithPropertiesAsync(_partition, "100", "200", null, false, tokenSource.Token) - .Returns(expected[3..6]); - _store - .GetInstanceIdentifierWithPropertiesAsync(_partition, "100", "201", null, false, tokenSource.Token) - .Returns(Array.Empty()); - - _store - .GetInstanceIdentifierWithPropertiesAsync(_partition, "1000", "2000", "3000", false, tokenSource.Token) - .Returns(new[] { expected[6] }); - _store - .GetInstanceIdentifierWithPropertiesAsync(_partition, "1000", "2000", "3001", false, tokenSource.Token) - .Returns(Array.Empty()); - - // Enumerate - var failures = new List(); - source.ReadFailure += (source, args) => failures.Add(args); - ReadResult[] actual = await source.ToArrayAsync(tokenSource.Token); - - // Check Results - await _store - .Received(1) - .GetInstanceIdentifierWithPropertiesAsync(_partition, "10", null, null, false, tokenSource.Token); - await _store - .Received(1) - .GetInstanceIdentifierWithPropertiesAsync(_partition, "11", null, null, false, tokenSource.Token); - await _store - .Received(1) - .GetInstanceIdentifierWithPropertiesAsync(_partition, "100", "200", null, false, tokenSource.Token); - await _store - .Received(1) - .GetInstanceIdentifierWithPropertiesAsync(_partition, "100", "201", null, false, tokenSource.Token); - await _store - .Received(1) - .GetInstanceIdentifierWithPropertiesAsync(_partition, "1000", "2000", "3000", false, tokenSource.Token); - await _store - .Received(1) - .GetInstanceIdentifierWithPropertiesAsync(_partition, "1000", "2000", "3001", false, tokenSource.Token); - - Assert.Same(expected[0], actual[0].Instance); - Assert.Same(expected[1], actual[1].Instance); - Assert.Same(expected[2], actual[2].Instance); - Assert.Equal(DicomIdentifier.ForStudy("11"), actual[3].Failure.Identifier); - Assert.Same(expected[3], actual[4].Instance); - Assert.Same(expected[4], actual[5].Instance); - Assert.Same(expected[5], actual[6].Instance); - Assert.Equal(DicomIdentifier.ForSeries("100", "201"), actual[7].Failure.Identifier); - Assert.Same(expected[6], actual[8].Instance); - Assert.Equal(DicomIdentifier.ForInstance("1000", "2000", "3001"), actual[9].Failure.Identifier); - - // Check event - Assert.Equal(3, failures.Count); - Assert.Equal(DicomIdentifier.ForStudy("11"), failures[0].Identifier); - Assert.Equal(DicomIdentifier.ForSeries("100", "201"), failures[1].Identifier); - Assert.Equal(DicomIdentifier.ForInstance("1000", "2000", "3001"), failures[2].Identifier); - } - - [Theory] - [InlineData(5, 0, null)] - [InlineData(100, 3, "1", "2", "3")] - [InlineData(2, 2, "4", "5", "6", "7")] - public async Task GivenSource_WhenFetchingBatch_ThenRemoveFromSource(int size, int expected, params string[] values) - { - DicomIdentifier[] identifierValues = values?.Select(DicomIdentifier.Parse).ToArray() ?? Array.Empty(); - _options.Values = identifierValues; - - using var tokenSource = new CancellationTokenSource(); - await using var source = new IdentifierExportSource(_store, _partition, _options); - - // Assert baseline - if (expected == 0) - Assert.Null(source.Description); - else - AssertConfiguration(identifierValues, source.Description); - - // Dequeue a batch - ExportDataOptions batch; - if (expected == 0) - { - Assert.False(source.TryDequeueBatch(size, out batch)); - Assert.Null(batch); - } - else - { - Assert.True(source.TryDequeueBatch(size, out batch)); - AssertConfiguration(identifierValues.Take(expected), batch); - - if (identifierValues.Length > expected) - AssertConfiguration(identifierValues.Skip(expected), source.Description); - else - Assert.Null(source.Description); - } - } - - private static void AssertConfiguration(IEnumerable expected, ExportDataOptions actual) - { - Assert.Equal(ExportSourceType.Identifiers, actual.Type); - - var options = actual.Settings as IdentifierExportOptions; - Assert.True(options.Values.SequenceEqual(expected)); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/ExtendedQueryTag/AddExtendedQueryTagEntryValidationTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/ExtendedQueryTag/AddExtendedQueryTagEntryValidationTests.cs deleted file mode 100644 index f17341ec70..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/ExtendedQueryTag/AddExtendedQueryTagEntryValidationTests.cs +++ /dev/null @@ -1,70 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.ExtendedQueryTag; - -public class AddExtendedQueryTagEntryValidationTests -{ - [Fact] - public void GivenValidAddExtendedQueryTagEntry_WhenValidating_ShouldSucced() - { - AddExtendedQueryTagEntry addExtendedQueryTagEntry = new AddExtendedQueryTagEntry() { Path = "00101001", Level = QueryTagLevel.Study }; - var validationContext = new ValidationContext(addExtendedQueryTagEntry); - IEnumerable results = addExtendedQueryTagEntry.Validate(validationContext); - Assert.Empty(results); - } - - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData(" ")] - public void GivenInvalidPath_WhenValidating_ResultShouldHaveExceptions(string pathValue) - { - AddExtendedQueryTagEntry addExtendedQueryTagEntry = new AddExtendedQueryTagEntry() { Path = pathValue, Level = QueryTagLevel.Study }; - var validationContext = new ValidationContext(addExtendedQueryTagEntry); - IEnumerable results = addExtendedQueryTagEntry.Validate(validationContext); - Assert.Single(results); - Assert.Equal("The Dicom Tag Property Path must be specified and must not be null, empty or whitespace.", results.First().ErrorMessage); - } - - [Fact] - public void GivenEmptyNullOrWhitespaceLevel_WhenValidating_ResultShouldHaveExceptions() - { - AddExtendedQueryTagEntry addExtendedQueryTagEntry = new AddExtendedQueryTagEntry() { Path = "00101001", Level = null }; - var validationContext = new ValidationContext(addExtendedQueryTagEntry); - IEnumerable results = addExtendedQueryTagEntry.Validate(validationContext); - Assert.Collection( - results, - item => Assert.Equal("The Dicom Tag Property Level must be specified and must not be null, empty or whitespace.", item.ErrorMessage)); - } - - [Fact] - public void GivenInvalidLevel_WhenValidating_ResultShouldHaveExceptions() - { - AddExtendedQueryTagEntry addExtendedQueryTagEntry = new AddExtendedQueryTagEntry() { Path = "00101001", Level = (QueryTagLevel)47 }; - var validationContext = new ValidationContext(addExtendedQueryTagEntry); - IEnumerable results = addExtendedQueryTagEntry.Validate(validationContext); - Assert.Single(results); - Assert.Equal("Input Dicom Tag Level '47' is invalid. It must have value 'Study', 'Series' or 'Instance'.", results.First().ErrorMessage); - } - - [Fact] - public void GivenMultipleValidationErrors_WhenValidating_ResultShouldHaveExceptions() - { - AddExtendedQueryTagEntry addExtendedQueryTagEntry = new AddExtendedQueryTagEntry() { Path = "", Level = null }; - var validationContext = new ValidationContext(addExtendedQueryTagEntry); - IEnumerable results = addExtendedQueryTagEntry.Validate(validationContext); - Assert.Collection( - results, - item => Assert.Equal("The Dicom Tag Property Path must be specified and must not be null, empty or whitespace.", item.ErrorMessage), - item => Assert.Equal("The Dicom Tag Property Level must be specified and must not be null, empty or whitespace.", item.ErrorMessage)); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/ExtendedQueryTag/AddExtendedQueryTagHandlerTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/ExtendedQueryTag/AddExtendedQueryTagHandlerTests.cs deleted file mode 100644 index acb9ac546d..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/ExtendedQueryTag/AddExtendedQueryTagHandlerTests.cs +++ /dev/null @@ -1,93 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Health.Core.Features.Security.Authorization; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Features.Security; -using Microsoft.Health.Dicom.Core.Messages.ExtendedQueryTag; -using Microsoft.Health.Operations; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.ExtendedQueryTag; - -public class AddExtendedQueryTagHandlerTests -{ - [Fact] - public void GivenNullArgument_WhenConstructing_ThenThrowArgumentNullException() - { - Assert.Throws( - () => new AddExtendedQueryTagHandler(null, Substitute.For())); - - Assert.Throws( - () => new AddExtendedQueryTagHandler(new DisabledAuthorizationService(), null)); - } - - [Fact] - public async Task GivenNullRequest_WhenHandlingRequest_ThenThrowArgumentNullException() - { - IAuthorizationService authService = Substitute.For>(); - IAddExtendedQueryTagService tagService = Substitute.For(); - var handler = new AddExtendedQueryTagHandler(authService, tagService); - - await Assert.ThrowsAsync(() => handler.Handle(null, default)); - - await authService.DidNotReceiveWithAnyArgs().CheckAccess(default, default); - await tagService.DidNotReceiveWithAnyArgs().AddExtendedQueryTagsAsync(default, default); - } - - [Fact] - public async Task GivenNoAccess_WhenHandlingRequest_ThenThrowUnauthorizedDicomActionException() - { - IAuthorizationService authService = Substitute.For>(); - IAddExtendedQueryTagService tagService = Substitute.For(); - var handler = new AddExtendedQueryTagHandler(authService, tagService); - - using var tokenSource = new CancellationTokenSource(); - - authService.CheckAccess(DataActions.ManageExtendedQueryTags, tokenSource.Token).Returns(DataActions.None); - await Assert.ThrowsAsync( - () => handler.Handle( - new AddExtendedQueryTagRequest(Array.Empty()), - tokenSource.Token)); - - await authService.Received(1).CheckAccess(DataActions.ManageExtendedQueryTags, tokenSource.Token); - await tagService.DidNotReceiveWithAnyArgs().AddExtendedQueryTagsAsync(default, default); - } - - [Fact] - public async Task GivenRequest_WhenHandlingRequest_ThenReturnResponse() - { - IAuthorizationService authService = Substitute.For>(); - IAddExtendedQueryTagService tagService = Substitute.For(); - var handler = new AddExtendedQueryTagHandler(authService, tagService); - - using var tokenSource = new CancellationTokenSource(); - - var input = new List { new AddExtendedQueryTagEntry() }; - var expected = new OperationReference(Guid.NewGuid(), new Uri("https://dicom/operation/status")); - authService.CheckAccess(DataActions.ManageExtendedQueryTags, tokenSource.Token).Returns(DataActions.ManageExtendedQueryTags); - tagService - .AddExtendedQueryTagsAsync( - Arg.Is>(x => ReferenceEquals(x, input)), - Arg.Is(tokenSource.Token)) - .Returns(Task.FromResult(expected)); - - AddExtendedQueryTagResponse actual = await handler.Handle( - new AddExtendedQueryTagRequest(input), - tokenSource.Token); - Assert.Same(expected, actual.Operation); - - await authService.Received(1).CheckAccess(DataActions.ManageExtendedQueryTags, tokenSource.Token); - await tagService.Received(1).AddExtendedQueryTagsAsync( - Arg.Is>(x => ReferenceEquals(x, input)), - Arg.Is(tokenSource.Token)); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/ExtendedQueryTag/AddExtendedQueryTagServiceTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/ExtendedQueryTag/AddExtendedQueryTagServiceTests.cs deleted file mode 100644 index 1f325c865d..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/ExtendedQueryTag/AddExtendedQueryTagServiceTests.cs +++ /dev/null @@ -1,174 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Threading; -using System.Threading.Tasks; -using FellowOakDicom; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Features.Operations; -using Microsoft.Health.Dicom.Core.Models.Operations; -using Microsoft.Health.Dicom.Tests.Common.Extensions; -using Microsoft.Health.Operations; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.ExtendedQueryTag; - -public class AddExtendedQueryTagServiceTests -{ - private readonly IExtendedQueryTagStore _extendedQueryTagStore; - private readonly IGuidFactory _guidFactory; - private readonly IDicomOperationsClient _client; - private readonly IExtendedQueryTagEntryValidator _extendedQueryTagEntryValidator; - private readonly AddExtendedQueryTagService _extendedQueryTagService; - private readonly CancellationTokenSource _tokenSource; - - public AddExtendedQueryTagServiceTests() - { - _extendedQueryTagStore = Substitute.For(); - _guidFactory = Substitute.For(); - _client = Substitute.For(); - _extendedQueryTagEntryValidator = Substitute.For(); - _extendedQueryTagService = new AddExtendedQueryTagService( - _extendedQueryTagStore, - _guidFactory, - _client, - _extendedQueryTagEntryValidator, - Options.Create(new ExtendedQueryTagConfiguration { MaxAllowedCount = 128 })); - - _tokenSource = new CancellationTokenSource(); - } - - [Fact] - public async Task GivenExistingOperation_WhenAddingExtendedQueryTag_ThenThrow() - { - DicomTag tag = DicomTag.DeviceSerialNumber; - AddExtendedQueryTagEntry entry = tag.BuildAddExtendedQueryTagEntry(); - - Guid id = Guid.NewGuid(); - var expected = new OperationReference(id, new Uri("https://dicom.contoso.io/unit/test/Operations/" + id, UriKind.Absolute)); - - var input = new AddExtendedQueryTagEntry[] { entry }; - _client - .FindOperationsAsync(Arg.Is(GetOperationPredicate()), _tokenSource.Token) - .Returns(new OperationReference[] { expected }.ToAsyncEnumerable()); - - await Assert.ThrowsAsync( - () => _extendedQueryTagService.AddExtendedQueryTagsAsync(input, _tokenSource.Token)); - - _client - .Received(1) - .FindOperationsAsync(Arg.Is(GetOperationPredicate()), _tokenSource.Token); - _extendedQueryTagEntryValidator.DidNotReceiveWithAnyArgs().ValidateExtendedQueryTags(default); - await _client.DidNotReceiveWithAnyArgs().StartReindexingInstancesAsync(default, default); - } - - [Fact] - public async Task GivenInvalidInput_WhenAddingExtendedQueryTag_ThenStopAfterValidation() - { - DicomTag tag = DicomTag.DeviceSerialNumber; - AddExtendedQueryTagEntry entry = tag.BuildAddExtendedQueryTagEntry(); - var exception = new ExtendedQueryTagEntryValidationException(string.Empty); - - _client - .FindOperationsAsync(Arg.Is(GetOperationPredicate()), _tokenSource.Token) - .Returns(AsyncEnumerable.Empty()); - - var input = new AddExtendedQueryTagEntry[] { entry }; - _extendedQueryTagEntryValidator.WhenForAnyArgs(v => v.ValidateExtendedQueryTags(input)).Throw(exception); - - await Assert.ThrowsAsync( - () => _extendedQueryTagService.AddExtendedQueryTagsAsync(input, _tokenSource.Token)); - - _client - .Received(1) - .FindOperationsAsync(Arg.Is(GetOperationPredicate()), _tokenSource.Token); - _extendedQueryTagEntryValidator.Received(1).ValidateExtendedQueryTags(input); - await _client.DidNotReceiveWithAnyArgs().StartReindexingInstancesAsync(default, default); - } - - [Fact] - public async Task GivenValidInput_WhenAddingExtendedQueryTag_ThenShouldSucceed() - { - DicomTag tag = DicomTag.DeviceSerialNumber; - AddExtendedQueryTagEntry entry = tag.BuildAddExtendedQueryTagEntry(); - ExtendedQueryTagStoreEntry storeEntry = tag.BuildExtendedQueryTagStoreEntry(); - - var input = new AddExtendedQueryTagEntry[] { entry }; - var operationId = Guid.NewGuid(); - var expected = new OperationReference( - operationId, - new Uri("https://dicom.contoso.io/unit/test/Operations/" + operationId, UriKind.Absolute)); - - _client - .FindOperationsAsync(Arg.Is(GetOperationPredicate()), _tokenSource.Token) - .Returns(AsyncEnumerable.Empty()); - _extendedQueryTagStore - .AddExtendedQueryTagsAsync( - Arg.Is>(x => x.Single().Path == entry.Path), - 128, - false, - _tokenSource.Token) - .Returns(new List { storeEntry }); - _guidFactory.Create().Returns(operationId); - _client - .StartReindexingInstancesAsync( - operationId, - Arg.Is>(x => x.Single() == storeEntry.Key), - _tokenSource.Token) - .Returns(expected); - _extendedQueryTagStore - .AssignReindexingOperationAsync( - Arg.Is>(x => x.Single() == storeEntry.Key), - operationId, - true, - _tokenSource.Token) - .Returns(new List { storeEntry }); - - OperationReference actual = await _extendedQueryTagService.AddExtendedQueryTagsAsync(input, _tokenSource.Token); - Assert.Same(expected, actual); - - _client - .Received(1) - .FindOperationsAsync(Arg.Is(GetOperationPredicate()), _tokenSource.Token); - _extendedQueryTagEntryValidator.Received(1).ValidateExtendedQueryTags(input); - await _extendedQueryTagStore - .Received(1) - .AddExtendedQueryTagsAsync( - Arg.Is>(x => x.Single().Path == entry.Path), - 128, - false, - _tokenSource.Token); - _guidFactory.Received(1).Create(); - await _client - .Received(1) - .StartReindexingInstancesAsync( - operationId, - Arg.Is>(x => x.Single() == storeEntry.Key), - _tokenSource.Token); - await _extendedQueryTagStore - .Received(1) - .AssignReindexingOperationAsync( - Arg.Is>(x => x.Single() == storeEntry.Key), - operationId, - true, - _tokenSource.Token); - } - - private static Expression>> GetOperationPredicate() - => (OperationQueryCondition x) => - x.CreatedTimeFrom == DateTime.MinValue && - x.CreatedTimeTo == DateTime.MaxValue && - x.Operations.Single() == DicomOperation.Reindex && - x.Statuses.SequenceEqual(new OperationStatus[] { OperationStatus.NotStarted, OperationStatus.Running }); -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/ExtendedQueryTag/DeleteExtendedQueryTagServiceTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/ExtendedQueryTag/DeleteExtendedQueryTagServiceTests.cs deleted file mode 100644 index 86cdfc0a4d..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/ExtendedQueryTag/DeleteExtendedQueryTagServiceTests.cs +++ /dev/null @@ -1,60 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading.Tasks; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Tests.Common.Extensions; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.ChangeFeed; - -public class DeleteExtendedQueryTagServiceTests -{ - private readonly IExtendedQueryTagStore _extendedQueryTagStore; - private readonly IDeleteExtendedQueryTagService _extendedQueryTagService; - - public DeleteExtendedQueryTagServiceTests() - { - _extendedQueryTagStore = Substitute.For(); - _extendedQueryTagService = new DeleteExtendedQueryTagService(_extendedQueryTagStore, new DicomTagParser()); - } - - [Fact] - public async Task GivenInputTagPath_WhenDeleteExtendedQueryTagIsInvoked_ThenShouldThrowException() - { - await Assert.ThrowsAsync(() => _extendedQueryTagService.DeleteExtendedQueryTagAsync("0000000A")); - } - - [Fact] - public async Task GivenNotExistingTagPath_WhenDeleteExtendedQueryTagIsInvoked_ThenShouldPassException() - { - string path = DicomTag.DeviceSerialNumber.GetPath(); - _extendedQueryTagStore - .GetExtendedQueryTagAsync(path, default) - .Returns(Task.FromException(new ExtendedQueryTagNotFoundException("Tag doesn't exist"))); - - await Assert.ThrowsAsync(() => _extendedQueryTagService.DeleteExtendedQueryTagAsync(path)); - - await _extendedQueryTagStore - .Received(1) - .GetExtendedQueryTagAsync(path, default); - } - - [Fact] - public async Task GivenValidTagPath_WhenDeleteExtendedQueryTagIsInvoked_ThenShouldSucceed() - { - DicomTag tag = DicomTag.DeviceSerialNumber; - string tagPath = tag.GetPath(); - var entry = new ExtendedQueryTagStoreJoinEntry(tag.BuildExtendedQueryTagStoreEntry()); - _extendedQueryTagStore.GetExtendedQueryTagAsync(tagPath, default).Returns(entry); - await _extendedQueryTagService.DeleteExtendedQueryTagAsync(tagPath); - await _extendedQueryTagStore.Received(1).DeleteExtendedQueryTagAsync(tagPath, entry.VR); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/ExtendedQueryTag/ExtendedQueryTagEntryValidatorTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/ExtendedQueryTag/ExtendedQueryTagEntryValidatorTests.cs deleted file mode 100644 index 7ea04802be..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/ExtendedQueryTag/ExtendedQueryTagEntryValidatorTests.cs +++ /dev/null @@ -1,200 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Tests.Common.Extensions; -using System.Globalization; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.ChangeFeed; - -public class ExtendedQueryTagEntryValidatorTests -{ - private readonly IExtendedQueryTagEntryValidator _extendedQueryTagEntryValidator; - - public ExtendedQueryTagEntryValidatorTests() - { - _extendedQueryTagEntryValidator = new ExtendedQueryTagEntryValidator(new DicomTagParser()); - } - - [Fact] - public void GivenNoExtendedQueryTagEntry_WhenValidating_ThenShouldThrowException() - { - Assert.Throws(() => { _extendedQueryTagEntryValidator.ValidateExtendedQueryTags(new AddExtendedQueryTagEntry[0]); }); - } - - [Fact] - public void GivenMissingLevel_WhenValidating_ThenShouldThrowException() - { - AddExtendedQueryTagEntry entry = new AddExtendedQueryTagEntry { Path = "00101060", VR = "PN" }; - Assert.Throws(() => _extendedQueryTagEntryValidator.ValidateExtendedQueryTags(new AddExtendedQueryTagEntry[] { entry })); - } - - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData("BABC")] - [InlineData("0018B001")] - public void GivenInvalidTag_WhenValidating_ThenShouldThrowException(string path) - { - AddExtendedQueryTagEntry entry = CreateExtendedQueryTagEntry(path, DicomVRCode.AE); - var ex = Assert.Throws(() => { _extendedQueryTagEntryValidator.ValidateExtendedQueryTags(new AddExtendedQueryTagEntry[] { entry }); }); - Assert.Equal(string.Format(CultureInfo.CurrentCulture, "The extended query tag '{0}' is invalid as it cannot be parsed into a valid Dicom Tag.", path), ex.Message); - } - - [Theory] - [InlineData("0074140c")] // lower case is also supported - [InlineData("0074140C")] - public void GivenValidTag_WhenValidating_ThenShouldSucceed(string path) - { - AddExtendedQueryTagEntry entry = CreateExtendedQueryTagEntry(path, DicomVRCode.IS); - _extendedQueryTagEntryValidator.ValidateExtendedQueryTags(new AddExtendedQueryTagEntry[] { entry }); - } - - [Theory] - [InlineData(null)] - [InlineData("")] - public void GivenStandardTagWithoutVR_WhenValidating_ThenShouldSucceed(string vr) - { - AddExtendedQueryTagEntry entry = CreateExtendedQueryTagEntry(DicomTag.DeviceSerialNumber.GetPath(), vr); - _extendedQueryTagEntryValidator.ValidateExtendedQueryTags(new AddExtendedQueryTagEntry[] { entry }); - } - - [Fact] - public void GivenStandardTagWithPrivateCreator_WhenValidating_ThenShouldThrowException() - { - AddExtendedQueryTagEntry entry = CreateExtendedQueryTagEntry(DicomTag.DeviceSerialNumber.GetPath(), null, privateCreator: "PrivateCreator"); - Assert.Throws(() => - { - _extendedQueryTagEntryValidator.ValidateExtendedQueryTags(new AddExtendedQueryTagEntry[] { entry }); - }); - } - - [Fact] - public void GivenInvalidVRForTag_WhenValidating_ThenShouldThrowException() - { - string tagPath = DicomTag.DeviceSerialNumber.GetPath(); - string vr = "CS"; // expected vr should be LO. CS is not acceptable - AddExtendedQueryTagEntry entry = CreateExtendedQueryTagEntry(tagPath, vr); - var ex = Assert.Throws(() => { _extendedQueryTagEntryValidator.ValidateExtendedQueryTags(new AddExtendedQueryTagEntry[] { entry }); }); - Assert.Equal(string.Format(CultureInfo.CurrentCulture, "The VR code '{0}' is incorrectly specified for '{1}'. The expected VR code for it is '{2}'. Retry this request either with the correct VR code or without specifying it.", vr, tagPath, "LO"), ex.Message); - } - - [Fact] - public void GivenInvalidVR_WhenValidating_ThenShouldThrowException() - { - string tagPath = DicomTag.DeviceSerialNumber.GetPath(); - string vr = "LOX"; - AddExtendedQueryTagEntry entry = CreateExtendedQueryTagEntry(tagPath, vr); - var ex = Assert.Throws(() => { _extendedQueryTagEntryValidator.ValidateExtendedQueryTags(new AddExtendedQueryTagEntry[] { entry }); }); - Assert.Equal(string.Format(CultureInfo.CurrentCulture, "The VR code '{0}' for tag '{1}' is invalid.", vr, tagPath), ex.Message); - } - - [Theory] - [InlineData("0018A001", DicomVRCode.SQ, DicomVRCode.SQ)] - [InlineData("0018A001", "", DicomVRCode.SQ)] // when VR is missing for standard tag - [InlineData("0040A30A", DicomVRCode.DS, DicomVRCode.DS)] - public void GivenUnsupportedVR_WhenValidating_ThenShouldThrowException(string path, string vr, string expectedVR) - { - AddExtendedQueryTagEntry entry = CreateExtendedQueryTagEntry(path, vr); - var ex = Assert.Throws(() => { _extendedQueryTagEntryValidator.ValidateExtendedQueryTags(new AddExtendedQueryTagEntry[] { entry }); }); - Assert.Equal(string.Format(CultureInfo.CurrentCulture, "The VR code '{0}' specified for tag '{1}' is not supported.", expectedVR, path), ex.Message); - } - - - - [Theory] - [InlineData("Lo")] // verify lower case - [InlineData("LO")] - public void GivenValidVR_WhenValidating_ThenShouldSucceed(string vr) - { - AddExtendedQueryTagEntry entry = CreateExtendedQueryTagEntry(DicomTag.DeviceSerialNumber.GetPath(), vr); - _extendedQueryTagEntryValidator.ValidateExtendedQueryTags(new AddExtendedQueryTagEntry[] { entry }); - } - - [Fact] - public void GivenPrivateTagWithoutVR_WhenValidating_ThenShouldThrowException() - { - string path = "12051003"; - string vr = string.Empty; - AddExtendedQueryTagEntry entry = CreateExtendedQueryTagEntry(path, vr, "PrivateCreator1"); - var ex = Assert.Throws(() => _extendedQueryTagEntryValidator.ValidateExtendedQueryTags(new AddExtendedQueryTagEntry[] { entry })); - Assert.Equal(string.Format(CultureInfo.CurrentCulture, "The vr for tag '12051003' is missing.", path), ex.Message); - } - - [Fact] - public void GivenPrivateTagWithoutPrivateCreator_WhenValidating_ThenShouldThrowException() - { - string path = "12051003"; - string vr = DicomVRCode.OB; - AddExtendedQueryTagEntry entry = CreateExtendedQueryTagEntry(path, vr); - var ex = Assert.Throws(() => { _extendedQueryTagEntryValidator.ValidateExtendedQueryTags(new AddExtendedQueryTagEntry[] { entry }); }); - Assert.Equal(string.Format(CultureInfo.CurrentCulture, "The private creator for private tag '{0}' is missing.", path), ex.Message); - } - - [Fact] - public void GivenPrivateTagWithTooLongPrivateCreator_WhenValidating_ThenShouldThrowException() - { - // max length of PrivateCreator is 64 - AddExtendedQueryTagEntry entry = CreateExtendedQueryTagEntry("12051003", DicomVRCode.CS, new string('c', 65)); - Assert.Throws(() => _extendedQueryTagEntryValidator.ValidateExtendedQueryTags(new AddExtendedQueryTagEntry[] { entry })); - } - - [Fact] - public void GivenPrivateTagWithVR_WhenValidating_ThenShouldSucceed() - { - AddExtendedQueryTagEntry entry = CreateExtendedQueryTagEntry("12051003", DicomVRCode.AE, "PrivateCreator1"); - _extendedQueryTagEntryValidator.ValidateExtendedQueryTags(new AddExtendedQueryTagEntry[] { entry }); - } - - [Fact] - public void GivenSupportedTag_WhenValidating_ThenShouldThrowException() - { - AddExtendedQueryTagEntry entry = DicomTag.PatientName.BuildAddExtendedQueryTagEntry(); - var ex = Assert.Throws(() => _extendedQueryTagEntryValidator.ValidateExtendedQueryTags(new AddExtendedQueryTagEntry[] { entry })); - Assert.Equal(string.Format(CultureInfo.CurrentCulture, "The query tag '{0}' is already supported.", entry.Path), ex.Message); - } - - [Fact] - public void GivenValidAndInvalidTags_WhenValidating_ThenShouldThrowException() - { - AddExtendedQueryTagEntry invalidEntry = DicomTag.PatientName.BuildAddExtendedQueryTagEntry(); - AddExtendedQueryTagEntry validEntry = DicomTag.DeviceSerialNumber.BuildAddExtendedQueryTagEntry(); - Assert.Throws(() => _extendedQueryTagEntryValidator.ValidateExtendedQueryTags(new AddExtendedQueryTagEntry[] { validEntry, invalidEntry })); - } - - [Fact] - public void GivenDuplicatedTag_WhenValidating_ThenShouldThrowException() - { - AddExtendedQueryTagEntry entry = DicomTag.PatientName.BuildAddExtendedQueryTagEntry(); - Assert.Throws(() => _extendedQueryTagEntryValidator.ValidateExtendedQueryTags(new AddExtendedQueryTagEntry[] { entry, entry })); - } - - [Fact] - public void GivenPrivateIdentificationCodeWithoutVR_WhenValidating_ThenShouldSucceed() - { - DicomTag dicomTag = new DicomTag(0x2201, 0x0010); - AddExtendedQueryTagEntry entry = CreateExtendedQueryTagEntry(dicomTag.GetPath(), null); - _extendedQueryTagEntryValidator.ValidateExtendedQueryTags(new AddExtendedQueryTagEntry[] { entry }); - } - - [Fact] - public void GivenPrivateIdentificationCodeWithWrongVR_WhenValidating_ThenShouldSucceed() - { - DicomTag dicomTag = new DicomTag(0x2201, 0x0010); - AddExtendedQueryTagEntry entry = CreateExtendedQueryTagEntry(dicomTag.GetPath(), DicomVR.AE.Code); - Assert.Throws(() => _extendedQueryTagEntryValidator.ValidateExtendedQueryTags(new AddExtendedQueryTagEntry[] { entry })); - } - - - private static AddExtendedQueryTagEntry CreateExtendedQueryTagEntry(string path, string vr, string privateCreator = null, QueryTagLevel level = QueryTagLevel.Instance) - { - return new AddExtendedQueryTagEntry { Path = path, VR = vr, PrivateCreator = privateCreator, Level = level }; - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/ExtendedQueryTag/ExtendedQueryTagErrorsServiceTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/ExtendedQueryTag/ExtendedQueryTagErrorsServiceTests.cs deleted file mode 100644 index 81181f2a3c..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/ExtendedQueryTag/ExtendedQueryTagErrorsServiceTests.cs +++ /dev/null @@ -1,133 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Features.Validation; -using Microsoft.Health.Dicom.Core.Messages.ExtendedQueryTag; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.ExtendedQueryTag; - -public class ExtendedQueryTagErrorsServiceTests -{ - private readonly IExtendedQueryTagErrorStore _extendedQueryTagErrorStore; - private readonly IDicomTagParser _dicomTagParser; - private readonly IExtendedQueryTagErrorsService _extendedQueryTagErrorsService; - private readonly CancellationTokenSource _tokenSource; - private readonly DateTime _definedNow; - - public ExtendedQueryTagErrorsServiceTests() - { - _extendedQueryTagErrorStore = Substitute.For(); - _dicomTagParser = Substitute.For(); - _extendedQueryTagErrorsService = new ExtendedQueryTagErrorsService(_extendedQueryTagErrorStore, _dicomTagParser); - _tokenSource = new CancellationTokenSource(); - _definedNow = DateTime.UtcNow; - } - - [Fact] - public async Task GivenValidInput_WhenAddingExtendedQueryTag_ThenShouldSucceed() - { - const int TagKey = 7; - const long Watermark = 30; - const ValidationErrorCode ErrorCode = ValidationErrorCode.DateIsInvalid; - - await _extendedQueryTagErrorsService.AddExtendedQueryTagErrorAsync( - TagKey, - ErrorCode, - Watermark, - _tokenSource.Token); - - await _extendedQueryTagErrorStore - .Received(1) - .AddExtendedQueryTagErrorAsync( - Arg.Is(TagKey), - Arg.Is(ErrorCode), - Arg.Is(Watermark), - Arg.Is(_tokenSource.Token)); - } - - [Fact] - public async Task GivenRequestForExtendedQueryTagError_WhenTagDoesNotExist_ThenReturnEmptyList() - { - string tagPath = DicomTag.DeviceID.GetPath(); - - DicomTag[] parsedTags = new DicomTag[] { DicomTag.DeviceID }; - - _dicomTagParser.TryParse(tagPath, out Arg.Any()).Returns(x => - { - x[1] = parsedTags; - return true; - }); - - _extendedQueryTagErrorStore.GetExtendedQueryTagErrorsAsync(tagPath, 100, 200).Returns(Array.Empty()); - GetExtendedQueryTagErrorsResponse response = await _extendedQueryTagErrorsService.GetExtendedQueryTagErrorsAsync(tagPath, 100, 200); - - _dicomTagParser.Received(1).TryParse( - Arg.Is(tagPath), - out Arg.Any()); - await _extendedQueryTagErrorStore.Received(1).GetExtendedQueryTagErrorsAsync(tagPath, 100, 200); - - Assert.Empty(response.ExtendedQueryTagErrors); - } - - [Fact] - public async Task GivenRequestForExtendedQueryTagError_WhenTagHasNoError_ThenReturnEmptyList() - { - string tagPath = DicomTag.DeviceID.GetPath(); - - DicomTag[] parsedTags = new DicomTag[] { DicomTag.DeviceID }; - - _dicomTagParser.TryParse(tagPath, out Arg.Any()).Returns(x => - { - x[1] = parsedTags; - return true; - }); - - _extendedQueryTagErrorStore.GetExtendedQueryTagErrorsAsync(tagPath, 10, 50).Returns(Array.Empty()); - await _extendedQueryTagErrorsService.GetExtendedQueryTagErrorsAsync(tagPath, 10, 50); - await _extendedQueryTagErrorStore.Received(1).GetExtendedQueryTagErrorsAsync(tagPath, 10, 50); - _dicomTagParser.Received(1).TryParse( - Arg.Is(tagPath), - out Arg.Any()); - } - - [Fact] - public async Task GivenRequestForExtendedQueryTagError_WhenTagExists_ThenReturnExtendedQueryTagErrorsList() - { - string tagPath = DicomTag.DeviceID.GetPath(); - - var expected = new List { new ExtendedQueryTagError( - DateTime.UtcNow, - Guid.NewGuid().ToString(), - Guid.NewGuid().ToString(), - Guid.NewGuid().ToString(), - "fake error message") }; - - DicomTag[] parsedTags = new DicomTag[] { DicomTag.DeviceID }; - - _dicomTagParser.TryParse(tagPath, out Arg.Any()).Returns(x => - { - x[1] = parsedTags; - return true; - }); - - _extendedQueryTagErrorStore.GetExtendedQueryTagErrorsAsync(tagPath, 5, 100).Returns(expected); - GetExtendedQueryTagErrorsResponse response = await _extendedQueryTagErrorsService.GetExtendedQueryTagErrorsAsync(tagPath, 5, 100); - await _extendedQueryTagErrorStore.Received(1).GetExtendedQueryTagErrorsAsync(tagPath, 5, 100); - _dicomTagParser.Received(1).TryParse( - Arg.Is(tagPath), - out Arg.Any()); - Assert.Equal(expected, response.ExtendedQueryTagErrors); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/ExtendedQueryTag/GetExtendedQueryTagsServiceTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/ExtendedQueryTag/GetExtendedQueryTagsServiceTests.cs deleted file mode 100644 index a642afc437..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/ExtendedQueryTag/GetExtendedQueryTagsServiceTests.cs +++ /dev/null @@ -1,126 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Features.Routing; -using Microsoft.Health.Dicom.Core.Messages.ExtendedQueryTag; -using Microsoft.Health.Dicom.Tests.Common.Comparers; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.ExtendedQueryTag; - -public class GetExtendedQueryTagsServiceTests -{ - private readonly IExtendedQueryTagStore _extendedQueryTagStore; - private readonly IDicomTagParser _dicomTagParser; - private readonly IUrlResolver _urlResolver; - private readonly IGetExtendedQueryTagsService _getExtendedQueryTagsService; - - public GetExtendedQueryTagsServiceTests() - { - _extendedQueryTagStore = Substitute.For(); - _dicomTagParser = Substitute.For(); - _urlResolver = Substitute.For(); - _getExtendedQueryTagsService = new GetExtendedQueryTagsService(_extendedQueryTagStore, _dicomTagParser, _urlResolver); - } - - [Fact] - public async Task GivenRequestForMultipleTags_WhenNoTagsAreStored_ThenReturnEmptyResult() - { - _extendedQueryTagStore.GetExtendedQueryTagsAsync(7, 0).Returns(Array.Empty()); - GetExtendedQueryTagsResponse response = await _getExtendedQueryTagsService.GetExtendedQueryTagsAsync(7, 0); - await _extendedQueryTagStore.Received(1).GetExtendedQueryTagsAsync(7, 0); - _urlResolver.DidNotReceiveWithAnyArgs().ResolveQueryTagErrorsUri(default); - - Assert.Empty(response.ExtendedQueryTags); - } - - [Fact] - public async Task GivenRequestForMultipleTags_WhenMultipleTagsAreStored_ThenExtendedQueryTagEntryListShouldBeReturned() - { - Guid operationId = Guid.NewGuid(); - ExtendedQueryTagStoreJoinEntry tag1 = CreateJoinEntry(1, "45456767", DicomVRCode.AE.ToString(), null, QueryTagLevel.Instance, ExtendedQueryTagStatus.Ready, 0, operationId); - ExtendedQueryTagStoreJoinEntry tag2 = CreateJoinEntry(2, "04051001", DicomVRCode.FL.ToString(), "PrivateCreator1", QueryTagLevel.Series, ExtendedQueryTagStatus.Adding, 7); - var operationUrl = new Uri("https://dicom.contoso.io/unit/test/operations/" + operationId.ToString("N"), UriKind.Absolute); - var tag2Errors = new Uri("https://dicom.contoso.io/unit/test/extendedquerytags/" + tag2.Path + "/errors", UriKind.Absolute); - - var storedEntries = new List() { tag1, tag2 }; - - _extendedQueryTagStore.GetExtendedQueryTagsAsync(101, 303).Returns(storedEntries); - _urlResolver.ResolveOperationStatusUri(operationId).Returns(operationUrl); - _urlResolver.ResolveQueryTagErrorsUri(tag2.Path).Returns(tag2Errors); - GetExtendedQueryTagsResponse response = await _getExtendedQueryTagsService.GetExtendedQueryTagsAsync(101, 303); - await _extendedQueryTagStore.Received(1).GetExtendedQueryTagsAsync(101, 303); - - var expected = new GetExtendedQueryTagEntry[] { tag1.ToGetExtendedQueryTagEntry(_urlResolver), tag2.ToGetExtendedQueryTagEntry(_urlResolver) }; - Assert.Equal(expected, response.ExtendedQueryTags, ExtendedQueryTagEntryEqualityComparer.Default); - _urlResolver.Received(2).ResolveOperationStatusUri(operationId); - _urlResolver.Received(2).ResolveQueryTagErrorsUri(tag2.Path); - } - - [Theory] - [InlineData("00181003")] - [InlineData("DeviceID")] - public async Task GivenRequestForExtendedQueryTag_WhenTagDoesntExist_ThenExceptionShouldBeThrown(string tagPath) - { - DicomTag[] parsedTags = new DicomTag[] { DicomTag.DeviceID }; - - _dicomTagParser.TryParse(tagPath, out Arg.Any()).Returns(x => - { - x[1] = parsedTags; - return true; - }); - - string actualTagPath = parsedTags[0].GetPath(); - _extendedQueryTagStore - .GetExtendedQueryTagAsync(actualTagPath, default) - .Returns(Task.FromException(new ExtendedQueryTagNotFoundException("Tag doesn't exist"))); - await Assert.ThrowsAsync(() => _getExtendedQueryTagsService.GetExtendedQueryTagAsync(tagPath)); - await _extendedQueryTagStore.Received(1).GetExtendedQueryTagAsync(actualTagPath, default); - _urlResolver.DidNotReceiveWithAnyArgs().ResolveQueryTagErrorsUri(default); - } - - [Fact] - public async Task GivenRequestForExtendedQueryTag_WhenTagExists_ThenExtendedQueryTagEntryShouldBeReturned() - { - string tagPath = DicomTag.DeviceID.GetPath(); - ExtendedQueryTagStoreJoinEntry stored = CreateJoinEntry(5, tagPath, DicomVRCode.AE.ToString()); - DicomTag[] parsedTags = new DicomTag[] { DicomTag.DeviceID }; - - _dicomTagParser.TryParse(tagPath, out Arg.Any()).Returns(x => - { - x[1] = parsedTags; - return true; - }); - - _extendedQueryTagStore.GetExtendedQueryTagAsync(tagPath, default).Returns(stored); - GetExtendedQueryTagResponse response = await _getExtendedQueryTagsService.GetExtendedQueryTagAsync(tagPath); - await _extendedQueryTagStore.Received(1).GetExtendedQueryTagAsync(tagPath, default); - _urlResolver.DidNotReceiveWithAnyArgs().ResolveQueryTagErrorsUri(default); - - Assert.Equal(stored.ToGetExtendedQueryTagEntry(), response.ExtendedQueryTag, ExtendedQueryTagEntryEqualityComparer.Default); - } - - private static ExtendedQueryTagStoreJoinEntry CreateJoinEntry( - int key, - string path, - string vr, - string privateCreator = null, - QueryTagLevel level = QueryTagLevel.Instance, - ExtendedQueryTagStatus status = ExtendedQueryTagStatus.Ready, - int errorCount = 0, - Guid? operationId = null) - { - return new ExtendedQueryTagStoreJoinEntry(key, path, vr, privateCreator, level, status, QueryStatus.Enabled, errorCount, operationId); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/ExtendedQueryTag/QueryTagServiceTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/ExtendedQueryTag/QueryTagServiceTests.cs deleted file mode 100644 index 2ef1070e06..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/ExtendedQueryTag/QueryTagServiceTests.cs +++ /dev/null @@ -1,36 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.ExtendedQueryTag; - -public class QueryTagServiceTests -{ - private readonly IExtendedQueryTagStore _extendedQueryTagStore; - private readonly IQueryTagService _queryTagService; - - public QueryTagServiceTests() - { - _extendedQueryTagStore = Substitute.For(); - _queryTagService = new QueryTagService(_extendedQueryTagStore); - } - - [Fact] - public async Task GivenValidInput_WhenGetExtendedQueryTagsIsCalledMultipleTimes_ThenExtendedQueryTagStoreIsCalledOnce() - { - _extendedQueryTagStore.GetExtendedQueryTagsAsync(int.MaxValue, 0, Arg.Any()) - .Returns(Array.Empty()); - - await _queryTagService.GetQueryTagsAsync(); - await _queryTagService.GetQueryTagsAsync(); - await _extendedQueryTagStore.Received(1).GetExtendedQueryTagsAsync(int.MaxValue, 0, Arg.Any()); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/ExtendedQueryTag/QueryTagTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/ExtendedQueryTag/QueryTagTests.cs deleted file mode 100644 index df9606671a..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/ExtendedQueryTag/QueryTagTests.cs +++ /dev/null @@ -1,55 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Tests.Common.Extensions; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.ExtendedQueryTag; - -public class QueryTagTests -{ - [Fact] - public void GivenCoreDicomTag_WhenInitialize_ThenShouldCreatedSuccessfully() - { - DicomTag tag = DicomTag.PatientName; - QueryTag queryTag = new QueryTag(tag); - Assert.Equal(tag, queryTag.Tag); - Assert.Equal(DicomVR.PN, queryTag.VR); - Assert.Null(queryTag.ExtendedQueryTagStoreEntry); - Assert.False(queryTag.IsExtendedQueryTag); - Assert.Equal(QueryTagLevel.Study, queryTag.Level); - } - - [Fact] - public void GivenStandardExtendedQueryTag_WhenInitialize_ThenShouldCreatedSuccessfully() - { - DicomTag tag = DicomTag.AcquisitionDate; - QueryTagLevel level = QueryTagLevel.Series; - var storeEntry = tag.BuildExtendedQueryTagStoreEntry(level: level); - QueryTag queryTag = new QueryTag(storeEntry); - Assert.Equal(tag, queryTag.Tag); - Assert.Equal(DicomVR.DA, queryTag.VR); - Assert.Equal(storeEntry, queryTag.ExtendedQueryTagStoreEntry); - Assert.True(queryTag.IsExtendedQueryTag); - Assert.Equal(level, queryTag.Level); - } - - [Fact] - public void GivenPrivateExtendedQueryTag_WhenInitialize_ThenShouldCreatedSuccessfully() - { - DicomTag tag = new DicomTag(0x1205, 0x1003, "PrivateCreator1"); - DicomVR vr = DicomVR.CS; - QueryTagLevel level = QueryTagLevel.Study; - var storeEntry = tag.BuildExtendedQueryTagStoreEntry(vr: vr.ToString(), privateCreator: tag.PrivateCreator.Creator, level: level); - QueryTag queryTag = new QueryTag(storeEntry); - Assert.Equal(tag, queryTag.Tag); - Assert.Equal(vr, queryTag.VR); - Assert.Equal(storeEntry, queryTag.ExtendedQueryTagStoreEntry); - Assert.True(queryTag.IsExtendedQueryTag); - Assert.Equal(level, queryTag.Level); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/FellowOak/CustomDicomImplementationTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/FellowOak/CustomDicomImplementationTests.cs deleted file mode 100644 index 96b24f3173..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/FellowOak/CustomDicomImplementationTests.cs +++ /dev/null @@ -1,61 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.IO; -using System.Threading.Tasks; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Features.FellowOakDicom; -using Microsoft.Health.Dicom.Tests.Common; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.FellowOak; - -public class CustomDicomImplementationTests -{ - private readonly DicomUID _expectedClassUID; - private readonly string _expectedVersion; - - public CustomDicomImplementationTests() - { - (DicomUID classUID, string versionName) = Samples.GetDicomImplemenationClasUIDAndVersionName(); - _expectedClassUID = classUID; - _expectedVersion = versionName; - CustomDicomImplementation.SetDicomImplementationClassUIDAndVersion(); - } - - [Fact] - public void SetFellowOakDicomImplementation_SetsClassUID() - { - Assert.Equal(_expectedClassUID, DicomImplementation.ClassUID); - } - - [Fact] - public void SetFellowOakDicomImplementation_SetsVersion() - { - Assert.Equal(_expectedVersion, DicomImplementation.Version); - } - - [Fact] - public async Task GivenDataset_WhenDicomFileIsSaved_DicomImplementationIsSetCorrectly() - { - var dataset = new DicomDataset - { - { DicomTag.SOPClassUID, TestUidGenerator.Generate() }, - { DicomTag.SOPInstanceUID, TestUidGenerator.Generate() } - }; - - CustomDicomImplementation.SetDicomImplementationClassUIDAndVersion(); - var dcmFile = new DicomFile(dataset); - - using var stream = new MemoryStream(); - await dcmFile.SaveAsync(stream); - stream.Seek(0, SeekOrigin.Begin); - - var actualDcmFile = await DicomFile.OpenAsync(stream); - - Assert.Equal(_expectedClassUID, actualDcmFile.FileMetaInfo.ImplementationClassUID); - Assert.Equal(_expectedVersion, actualDcmFile.FileMetaInfo.ImplementationVersionName); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Operations/OperationStateHandlerTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Operations/OperationStateHandlerTests.cs deleted file mode 100644 index 8b86baed4a..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Operations/OperationStateHandlerTests.cs +++ /dev/null @@ -1,74 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Health.Core.Features.Security.Authorization; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Operations; -using Microsoft.Health.Dicom.Core.Features.Security; -using Microsoft.Health.Dicom.Core.Messages.Operations; -using Microsoft.Health.Dicom.Core.Models.Operations; -using Microsoft.Health.Operations; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Operations; - -public class OperationStateHandlerTests -{ - [Fact] - public void GivenNullArgument_WhenConstructing_ThenThrowArgumentNullException() - { - Assert.Throws(() => new OperationStateHandler(null, Substitute.For())); - Assert.Throws(() => new OperationStateHandler(Substitute.For>(), null)); - } - - [Fact] - public async Task GivenInvalidAuthorization_WhenHandlingRequest_ThenThrowUnauthorizedDicomActionException() - { - using var source = new CancellationTokenSource(); - IAuthorizationService auth = Substitute.For>(); - IDicomOperationsClient client = Substitute.For(); - var handler = new OperationStateHandler(auth, client); - - auth.CheckAccess(DataActions.Read, source.Token).Returns(DataActions.None); - - await Assert.ThrowsAsync(() => handler.Handle(new OperationStateRequest(Guid.NewGuid()), source.Token)); - - await auth.Received(1).CheckAccess(DataActions.Read, source.Token); - await client.DidNotReceiveWithAnyArgs().GetStateAsync(default, default); - } - - [Fact] - public async Task GivenValidRequest_WhenHandlingRequest_ThenReturnResponse() - { - using var source = new CancellationTokenSource(); - IAuthorizationService auth = Substitute.For>(); - IDicomOperationsClient client = Substitute.For(); - var handler = new OperationStateHandler(auth, client); - - Guid id = Guid.NewGuid(); - var expected = new OperationState - { - CreatedTime = DateTime.UtcNow.AddMinutes(-5), - LastUpdatedTime = DateTime.UtcNow, - OperationId = id, - PercentComplete = 100, - Resources = new Uri[] { new Uri("https://dicom.contoso.io/unit/test/extendedquerytags/00101010", UriKind.Absolute) }, - Status = OperationStatus.Succeeded, - Type = DicomOperation.Reindex, - }; - - auth.CheckAccess(DataActions.Read, source.Token).Returns(DataActions.Read); - client.GetStateAsync(id, source.Token).Returns(expected); - - Assert.Same(expected, (await handler.Handle(new OperationStateRequest(id), source.Token)).OperationState); - - await auth.Received(1).CheckAccess(DataActions.Read, source.Token); - await client.Received(1).GetStateAsync(id, source.Token); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Partitioning/PartitionCacheTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Partitioning/PartitionCacheTests.cs deleted file mode 100644 index ea605aa449..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Partitioning/PartitionCacheTests.cs +++ /dev/null @@ -1,45 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Partitioning; - -public class PartitionCacheTests -{ - [Fact] - public async Task GivenMultipleThreadsExecuteGetOrAddPartitionAsync_OnlyOnceActionShouldExecute() - { - var config = Substitute.For>(); - config.Value.Returns(new DataPartitionConfiguration()); - - var logger = Substitute.For>(); - var partitionCache = new PartitionCache(config, Substitute.For(), logger); - - int numExecuted = 0; - - Func> mockAction = async (string partitionName, CancellationToken cancellationToken) => - { - await Task.Delay(200, cancellationToken); - numExecuted++; - return new Partition(1, partitionName); - }; - - var threadList = Enumerable.Range(0, 5).Select(async _ => await partitionCache.GetAsync("", "", mockAction, CancellationToken.None)).ToList(); - - await Task.WhenAll(threadList); - - Assert.Equal(1, numExecuted); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Partitioning/PartitionServiceTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Partitioning/PartitionServiceTests.cs deleted file mode 100644 index 6c47242dcb..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Partitioning/PartitionServiceTests.cs +++ /dev/null @@ -1,100 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Core.Messages.Partitioning; -using NSubstitute; -using NSubstitute.ExceptionExtensions; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Partitioning; - -public class PartitionServiceTests -{ - private readonly IOptions _partitionCacheOptions; - private readonly IPartitionStore _partitionStore; - private readonly PartitionCache _partitionCache; - private readonly PartitionService _partitionService; - - public PartitionServiceTests() - { - _partitionCacheOptions = Substitute.For>(); - _partitionCacheOptions.Value.Returns(new DataPartitionConfiguration()); - _partitionStore = Substitute.For(); - _partitionCache = new PartitionCache(_partitionCacheOptions, new LoggerFactory(), NullLogger.Instance); - _partitionService = new PartitionService(_partitionCache, _partitionStore, NullLogger.Instance); - } - - [Fact] - public async Task GivenAGetOrAddRequest_WhenPartitionExists_ReturnsPartition() - { - var returnThis = new Partition(1, "test", DateTimeOffset.Now); - _partitionStore.GetPartitionAsync("test", Arg.Any()).Returns(returnThis); - - GetOrAddPartitionResponse result = await _partitionService.GetOrAddPartitionAsync("test", CancellationToken.None); - - Assert.Equal("test", result.Partition.Name); - Assert.Equal(1, result.Partition.Key); - - await _partitionStore.DidNotReceiveWithAnyArgs().AddPartitionAsync(Arg.Any(), Arg.Any()); - } - - [Fact] - public async Task GivenANonExistingPartition_WhenAttemptingToGet_ThrowsDataPartitionNotFound() - { - _partitionStore.GetPartitionAsync("notfound", CancellationToken.None).Returns((Partition)null); - - await Assert.ThrowsAsync(() => _partitionService.GetPartitionAsync("notfound", CancellationToken.None)); - } - - [Fact] - public async Task GivenAnInvalidPartition_WhenAttemptingToGet_ThrowsInvalidPartition() - { - await Assert.ThrowsAsync(() => _partitionService.GetPartitionAsync("test#$", CancellationToken.None)); - } - - [Fact] - public async Task GivenAnInvalidPartition_WhenAttemptingToGetOrAdd_ThrowsInvalidPartition() - { - await Assert.ThrowsAsync(() => _partitionService.GetOrAddPartitionAsync("test#$", CancellationToken.None)); - } - - [Fact] - public async Task GivenAGetOrAddRequest_WhenPartitionDoesntExist_CreatesAndReturnsPartition() - { - var returnThis = new Partition(1, "test", DateTimeOffset.Now); - _partitionStore.GetPartitionAsync("test", Arg.Any()).Returns((Partition)null); - _partitionStore.AddPartitionAsync("test", Arg.Any()).Returns(returnThis); - - GetOrAddPartitionResponse result = await _partitionService.GetOrAddPartitionAsync("test", CancellationToken.None); - - Assert.Equal("test", result.Partition.Name); - Assert.Equal(1, result.Partition.Key); - - await _partitionStore.Received(1).AddPartitionAsync("test", Arg.Any()); - } - - [Fact] - public async Task GivenAGetOrAddRequest_WhenPartitionCreatedInMeantime_ReturnsPartition() - { - var returnThis = new Partition(1, "test", DateTimeOffset.Now); - _partitionStore.GetPartitionAsync("test", Arg.Any()) - .Returns(_ => null, _ => returnThis); - _partitionStore.AddPartitionAsync("test", Arg.Any()).ThrowsAsyncForAnyArgs(new DataPartitionAlreadyExistsException()); - - GetOrAddPartitionResponse result = await _partitionService.GetOrAddPartitionAsync("test", CancellationToken.None); - - Assert.Equal("test", result.Partition.Name); - Assert.Equal(1, result.Partition.Key); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Query/QueryParserTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Query/QueryParserTests.cs deleted file mode 100644 index 38efd9866f..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Query/QueryParserTests.cs +++ /dev/null @@ -1,583 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using EnsureThat; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Features.Query; -using Microsoft.Health.Dicom.Core.Features.Query.Model; -using Microsoft.Health.Dicom.Tests.Common; -using Microsoft.Health.Dicom.Tests.Common.Extensions; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Query; - -public class QueryParserTests -{ - private readonly QueryParser _queryParser; - - public QueryParserTests() - { - _queryParser = new QueryParser(new DicomTagParser()); - } - - [Fact] - public void GivenParameters_WhenParsing_ThenForwardValues() - { - var parameters = new QueryParameters - { - Filters = new Dictionary(), - FuzzyMatching = true, - IncludeField = Array.Empty(), - Limit = 12, - Offset = 700, - }; - - QueryExpression actual = _queryParser.Parse(parameters, Array.Empty()); - Assert.Equal(parameters.FuzzyMatching, actual.FuzzyMatching); - Assert.Equal(parameters.Limit, actual.Limit); - Assert.Equal(parameters.Offset, actual.Offset); - } - - [Theory] - [InlineData("StudyDate")] - [InlineData("00100020")] - [InlineData("00100020,00100010")] - [InlineData("StudyDate,StudyTime")] - public void GivenIncludeField_WithValidAttributeId_CheckIncludeFields(string value) - { - EnsureArg.IsNotNull(value, nameof(value)); - VerifyIncludeFieldsForValidAttributeIds(value.Split(',')); - } - - [Fact] - public void GivenIncludeField_WithValueAll_CheckAllValue() - { - QueryExpression queryExpression = _queryParser.Parse( - CreateParameters(new Dictionary(), QueryResource.AllStudies, includeField: new string[] { "all" }), - QueryTagService.CoreQueryTags); - Assert.True(queryExpression.IncludeFields.All); - } - - [Fact] - public void GivenIncludeField_WithInvalidAttributeId_Throws() - { - Assert.Throws(() => _queryParser.Parse( - CreateParameters(new Dictionary(), QueryResource.AllStudies, includeField: new string[] { "something" }), - QueryTagService.CoreQueryTags)); - } - - [Theory] - [InlineData("12050010")] - [InlineData("12051001")] - public void GivenIncludeField_WithPrivateAttributeId_CheckIncludeFields(string value) - { - VerifyIncludeFieldsForValidAttributeIds(value); - } - - [Theory] - [InlineData("includefield", "12345678")] - [InlineData("includefield", "98765432")] - public void GivenIncludeField_WithUnknownAttributeId_Throws(string key, string value) - { - Assert.Throws(() => _queryParser - .Parse(CreateParameters(GetSingleton(key, value), QueryResource.AllStudies), QueryTagService.CoreQueryTags)); - } - - [Theory] - [InlineData("PatientName", "joe\"s")] - public void GivenFilterCondition_InvalidFuzzyMatchTagValue_Throws(string key, string value) - { - Assert.Throws(() => _queryParser - .Parse(CreateParameters(GetSingleton(key, value), QueryResource.AllStudies, null, null, true), QueryTagService.CoreQueryTags)); - } - - - [Theory] - [InlineData("00100010", "joe")] - [InlineData("PatientName", "joe")] - public void GivenFilterCondition_ValidTag_CheckProperties(string key, string value) - { - QueryExpression queryExpression = _queryParser - .Parse(CreateParameters(GetSingleton(key, value), QueryResource.AllStudies), QueryTagService.CoreQueryTags); - Assert.True(queryExpression.HasFilters); - var singleValueCond = queryExpression.FilterConditions.First() as StringSingleValueMatchCondition; - Assert.NotNull(singleValueCond); - Assert.True(singleValueCond.QueryTag.Tag == DicomTag.PatientName); - Assert.True(singleValueCond.Value == value); - } - - [Theory] - [InlineData("ReferringPhysicianName", "dr^joe")] - public void GivenFilterCondition_ValidReferringPhysicianNameTag_CheckProperties(string key, string value) - { - QueryExpression queryExpression = _queryParser - .Parse(CreateParameters(GetSingleton(key, value), QueryResource.AllStudies), QueryTagService.CoreQueryTags); - Assert.True(queryExpression.HasFilters); - var singleValueCond = queryExpression.FilterConditions.First() as StringSingleValueMatchCondition; - Assert.NotNull(singleValueCond); - Assert.Equal(DicomTag.ReferringPhysicianName, singleValueCond.QueryTag.Tag); - Assert.Equal(value, singleValueCond.Value); - } - - [Theory] - [InlineData("00201208", "3")] - public void GivenFilterCondition_WithNotSupportedTag_Throws(string key, string value) - { - Assert.Throws(() => _queryParser - .Parse(CreateParameters(GetSingleton(key, value), QueryResource.AllStudies), QueryTagService.CoreQueryTags)); - } - - [Theory] - [InlineData("ModalitiesInStudy", "CT")] - public void GivenFilterCondition_WithModalitiesInStudySupportedTag_Works(string key, string value) - { - var queryExp = _queryParser - .Parse(CreateParameters(GetSingleton(key, value), QueryResource.AllStudies), QueryTagService.CoreQueryTags); - - QueryFilterCondition queryFilterCondition = queryExp.FilterConditions.First(); - Assert.True(queryFilterCondition is StudyToSeriesStringSingleValueMatchCondition); - Assert.True(queryFilterCondition.QueryTag.Tag == DicomTag.ModalitiesInStudy); - } - - [Theory] - [InlineData("Modality", "CT", QueryResource.AllStudies)] - [InlineData("SOPInstanceUID", "1.2.3.48898989", QueryResource.AllSeries)] - [InlineData("PatientName", "Joe", QueryResource.StudySeries)] - [InlineData("Modality", "CT", QueryResource.StudySeriesInstances)] - public void GivenFilterCondition_WithKnownTagButNotSupportedAtLevel_Throws(string key, string value, QueryResource resourceType) - { - Assert.Throws(() => _queryParser - .Parse(CreateParameters(GetSingleton(key, value), resourceType), QueryTagService.CoreQueryTags)); - } - - [Fact] - public void GivenExtendedQueryPrivateTag_WithUrl_ParseSucceeds() - { - DicomTag tag = new DicomTag(0x0405, 0x1001, "PrivateCreator1"); - QueryTag queryTag = new QueryTag(tag.BuildExtendedQueryTagStoreEntry(vr: DicomVRCode.CS, level: QueryTagLevel.Study)); - - QueryExpression queryExpression = _queryParser.Parse(CreateParameters(GetSingleton(tag.GetPath(), "Test"), QueryResource.AllStudies), new[] { queryTag }); - Assert.Equal(queryTag, queryExpression.FilterConditions.First().QueryTag); - } - - [Fact] - public void GivenExtendedQueryDateTag_WithUrl_ParseSucceeds() - { - QueryTag queryTag = new QueryTag(DicomTag.Date.BuildExtendedQueryTagStoreEntry(level: QueryTagLevel.Study)); - - QueryExpression queryExpression = _queryParser.Parse(CreateParameters(GetSingleton("Date", "19510910-20200220"), QueryResource.AllStudies), new[] { queryTag }); - Assert.Equal(queryTag, queryExpression.FilterConditions.First().QueryTag); - } - - [Fact] - public void GivenExtendedQueryDateTimeTag_WithUrl_ParseSucceeds() - { - QueryTag queryTag = new QueryTag(DicomTag.DateTime.BuildExtendedQueryTagStoreEntry(level: QueryTagLevel.Study)); - - QueryExpression queryExpression = _queryParser.Parse(CreateParameters(GetSingleton("DateTime", "20200301195109.10-20200501195110.20"), QueryResource.AllStudies), new[] { queryTag }); - Assert.Equal(queryTag, queryExpression.FilterConditions.First().QueryTag); - } - - [Theory] - [InlineData("19510910010203", "20200220020304")] - public void GivenDateTime_WithValidRangeMatch_CheckCondition(string minValue, string maxValue) - { - EnsureArg.IsNotNull(minValue, nameof(minValue)); - EnsureArg.IsNotNull(maxValue, nameof(maxValue)); - QueryTag queryTag = new QueryTag(DicomTag.DateTime.BuildExtendedQueryTagStoreEntry(level: QueryTagLevel.Study)); - - QueryExpression queryExpression = _queryParser.Parse(CreateParameters(GetSingleton("DateTime", string.Concat(minValue, "-", maxValue)), QueryResource.AllStudies), new[] { queryTag }); - var cond = queryExpression.FilterConditions.First() as DateRangeValueMatchCondition; - Assert.NotNull(cond); - Assert.True(cond.QueryTag.Tag == DicomTag.DateTime); - Assert.True(cond.Minimum == DateTime.ParseExact(minValue, QueryParser.DateTimeTagValueFormats, null)); - Assert.True(cond.Maximum == DateTime.ParseExact(maxValue, QueryParser.DateTimeTagValueFormats, null)); - } - - [Theory] - [InlineData("", "20200220020304")] - [InlineData("19510910010203", "")] - public void GivenDateTime_WithEmptyMinOrMaxValueInRangeMatch_CheckCondition(string minValue, string maxValue) - { - EnsureArg.IsNotNull(minValue, nameof(minValue)); - EnsureArg.IsNotNull(maxValue, nameof(maxValue)); - QueryTag queryTag = new QueryTag(DicomTag.DateTime.BuildExtendedQueryTagStoreEntry(level: QueryTagLevel.Study)); - - QueryExpression queryExpression = _queryParser.Parse(CreateParameters(GetSingleton("DateTime", string.Concat(minValue, "-", maxValue)), QueryResource.AllStudies), new[] { queryTag }); - var cond = queryExpression.FilterConditions.First() as DateRangeValueMatchCondition; - Assert.NotNull(cond); - Assert.Equal(DicomTag.DateTime, cond.QueryTag.Tag); - - DateTime expectedMin = string.IsNullOrEmpty(minValue) ? DateTime.MinValue : DateTime.ParseExact(minValue, QueryParser.DateTimeTagValueFormats, null); - DateTime expectedMax = string.IsNullOrEmpty(maxValue) ? DateTime.MaxValue : DateTime.ParseExact(maxValue, QueryParser.DateTimeTagValueFormats, null); - Assert.Equal(expectedMin, cond.Minimum); - Assert.Equal(expectedMax, cond.Maximum); - } - - [Fact] - public void GivenDateTime_WithEmptyMinAndMaxInRangeMatch_Throw() - { - QueryTag queryTag = new QueryTag(DicomTag.DateTime.BuildExtendedQueryTagStoreEntry(level: QueryTagLevel.Study)); - Assert.Throws(() => _queryParser - .Parse(CreateParameters(GetSingleton("DateTime", "-"), QueryResource.AllStudies), new[] { queryTag })); - } - - [Fact] - public void GivenExtendedQueryTimeTag_WithUrl_ParseSucceeds() - { - QueryTag queryTag = new QueryTag(DicomTag.Time.BuildExtendedQueryTagStoreEntry(level: QueryTagLevel.Study)); - - QueryExpression queryExpression = _queryParser.Parse(CreateParameters(GetSingleton("Time", "195109.10-195110.20"), QueryResource.AllStudies), new[] { queryTag }); - Assert.Equal(queryTag, queryExpression.FilterConditions.First().QueryTag); - } - - [Theory] - [InlineData("010203", "020304")] - public void GivenStudyTime_WithValidRangeMatch_CheckCondition(string minValue, string maxValue) - { - EnsureArg.IsNotNull(minValue, nameof(minValue)); - EnsureArg.IsNotNull(maxValue, nameof(maxValue)); - QueryTag queryTag = new QueryTag(DicomTag.Time.BuildExtendedQueryTagStoreEntry(level: QueryTagLevel.Study)); - - QueryExpression queryExpression = _queryParser.Parse(CreateParameters(GetSingleton("Time", string.Concat(minValue, "-", maxValue)), QueryResource.AllStudies), new[] { queryTag }); - var cond = queryExpression.FilterConditions.First() as LongRangeValueMatchCondition; - Assert.NotNull(cond); - Assert.Equal(DicomTag.Time, cond.QueryTag.Tag); - - long minTicks = new DicomTime(cond.QueryTag.Tag, new string[] { minValue }).Get().Ticks; - long maxTicks = new DicomTime(cond.QueryTag.Tag, new string[] { maxValue }).Get().Ticks; - - Assert.Equal(minTicks, cond.Minimum); - Assert.Equal(maxTicks, cond.Maximum); - } - - [Theory] - [InlineData("", "020304")] - [InlineData("010203", "")] - public void GivenStudyTime_WithEmptyMinOrMaxValueInRangeMatch_CheckCondition(string minValue, string maxValue) - { - EnsureArg.IsNotNull(minValue, nameof(minValue)); - EnsureArg.IsNotNull(maxValue, nameof(maxValue)); - QueryTag queryTag = new QueryTag(DicomTag.Time.BuildExtendedQueryTagStoreEntry(level: QueryTagLevel.Study)); - - QueryExpression queryExpression = _queryParser.Parse(CreateParameters(GetSingleton("Time", string.Concat(minValue, "-", maxValue)), QueryResource.AllStudies), new[] { queryTag }); - var cond = queryExpression.FilterConditions.First() as LongRangeValueMatchCondition; - Assert.NotNull(cond); - Assert.True(cond.QueryTag.Tag == DicomTag.Time); - - long minTicks = string.IsNullOrEmpty(minValue) ? 0 : new DicomTime(cond.QueryTag.Tag, new string[] { minValue }).Get().Ticks; - long maxTicks = string.IsNullOrEmpty(maxValue) ? TimeSpan.TicksPerDay : new DicomTime(cond.QueryTag.Tag, new string[] { maxValue }).Get().Ticks; - - Assert.Equal(minTicks, cond.Minimum); - Assert.Equal(maxTicks, cond.Maximum); - } - - [Fact] - public void GivenStudyTime_WithEmptyMinAndMaxInRangeMatch_Throw() - { - Assert.Throws(() => _queryParser - .Parse(CreateParameters(GetSingleton("StudyTime", "-"), QueryResource.AllSeries), QueryTagService.CoreQueryTags)); - } - - [Fact] - public void GivenExtendedQueryPersonNameTag_WithUrl_ParseSucceeds() - { - QueryTag queryTag = new QueryTag(DicomTag.PatientBirthName.BuildExtendedQueryTagStoreEntry(level: QueryTagLevel.Series)); - - QueryExpression queryExpression = _queryParser.Parse( - CreateParameters(GetSingleton(nameof(DicomTag.PatientBirthName), "Joe"), QueryResource.AllSeries, fuzzyMatching: true), - new[] { queryTag }); - var fuzzyCondition = queryExpression.FilterConditions.First() as PersonNameFuzzyMatchCondition; - Assert.NotNull(fuzzyCondition); - Assert.Equal("Joe", fuzzyCondition.Value); - Assert.Equal(queryTag, fuzzyCondition.QueryTag); - } - - [Fact] - public void GivenExtendedQueryStringTag_WithUrl_ParseSucceeds() - { - QueryTag queryTag = new QueryTag(DicomTag.ModelGroupUID.BuildExtendedQueryTagStoreEntry(level: QueryTagLevel.Series)); - QueryExpression queryExpression = _queryParser.Parse(CreateParameters(GetSingleton(nameof(DicomTag.ModelGroupUID), "abc"), QueryResource.AllSeries), new[] { queryTag }); - Assert.Equal(queryTag, queryExpression.FilterConditions.First().QueryTag); - } - - [Fact] - public void GivenExtendedQueryStringTag_WithTagPathUrl_ParseSucceeds() - { - QueryTag queryTag = new QueryTag(DicomTag.ModelGroupUID.BuildExtendedQueryTagStoreEntry(level: QueryTagLevel.Series)); - QueryExpression queryExpression = _queryParser.Parse(CreateParameters(GetSingleton("00687004", "abc"), QueryResource.AllSeries), new[] { queryTag }); - Assert.Equal(queryTag, queryExpression.FilterConditions.First().QueryTag); - } - - [Fact] - public void GivenExtendedQueryLongTag_WithUrl_ParseSucceeds() - { - QueryTag queryTag = new QueryTag(DicomTag.NumberOfAssessmentObservations.BuildExtendedQueryTagStoreEntry(level: QueryTagLevel.Series)); - QueryExpression queryExpression = _queryParser.Parse(CreateParameters(GetSingleton(nameof(DicomTag.NumberOfAssessmentObservations), "50"), QueryResource.AllSeries), new[] { queryTag }); - Assert.Equal(queryTag, queryExpression.FilterConditions.First().QueryTag); - } - - [Fact] - public void GivenExtendedQueryDoubleTag_WithUrl_ParseSucceeds() - { - QueryTag queryTag = new QueryTag(DicomTag.FloatingPointValue.BuildExtendedQueryTagStoreEntry(level: QueryTagLevel.Series)); - QueryExpression queryExpression = _queryParser.Parse(CreateParameters(GetSingleton(nameof(DicomTag.FloatingPointValue), "1.1"), QueryResource.AllSeries), new[] { queryTag }); - Assert.Equal(queryTag, queryExpression.FilterConditions.First().QueryTag); - } - - [Fact] - public void GivenExtendedQueryDoubleTagWithInvalidValue_WithUrl_ParseFails() - { - QueryTag queryTag = new QueryTag(DicomTag.FloatingPointValue.BuildExtendedQueryTagStoreEntry(level: QueryTagLevel.Series)); - Assert.Throws(() => _queryParser - .Parse(CreateParameters(GetSingleton(nameof(DicomTag.FloatingPointValue), "abc"), QueryResource.AllStudies), new[] { queryTag })); - } - - [Fact] - public void GivenNonExistingExtendedQueryStringTag_WithUrl_ParseFails() - { - QueryTag queryTag = new QueryTag(DicomTag.FloatingPointValue.BuildExtendedQueryTagStoreEntry(level: QueryTagLevel.Series)); - Assert.Throws(() => _queryParser - .Parse(CreateParameters(GetSingleton(nameof(DicomTag.ModelGroupUID), "abc"), QueryResource.AllStudies), new[] { queryTag })); - } - - [Fact] - public void GivenCombinationOfExtendedQueryAndStandardTags_WithUrl_ParseSucceeds() - { - QueryTag queryTag1 = new QueryTag(DicomTag.FloatingPointValue.BuildExtendedQueryTagStoreEntry(level: QueryTagLevel.Series)); - QueryTag queryTag2 = new QueryTag(DicomTag.ModelGroupUID.BuildExtendedQueryTagStoreEntry(level: QueryTagLevel.Series)); - QueryExpression queryExpression = _queryParser.Parse( - CreateParameters( - new Dictionary - { - { "PatientName", "Joe" }, - { "FloatingPointValue", "1.1" }, - { "StudyDate", "19510910-20200220" }, - { "00687004", "abc" }, - }, - QueryResource.AllSeries), - QueryTagService.CoreQueryTags.Concat(new[] { queryTag1, queryTag2 }).ToList()); - Assert.Equal(4, queryExpression.FilterConditions.Count); - Assert.Contains(queryTag1, queryExpression.FilterConditions.Select(x => x.QueryTag)); - Assert.Contains(queryTag2, queryExpression.FilterConditions.Select(x => x.QueryTag)); - } - - [Fact] - public void GivenFilterCondition_WithDuplicateQueryParam_Throws() - { - Assert.Throws(() => _queryParser.Parse( - CreateParameters( - new Dictionary - { - { "PatientName", "Joe" }, - { "00100010", "Rob" }, - }, - QueryResource.AllStudies), - QueryTagService.CoreQueryTags)); - } - - [Theory] - [InlineData("PatientName", " ")] - [InlineData("StudyDescription", "")] - public void GivenFilterCondition_WithInvalidAttributeIdStringValue_Throws(string key, string value) - { - Assert.Throws(() => _queryParser - .Parse(CreateParameters(GetSingleton(key, value), QueryResource.AllStudies), QueryTagService.CoreQueryTags)); - } - - [Theory] - [InlineData("00390061", "invalidtag")] - [InlineData("unkownparam", "invalidtag")] - public void GivenFilterCondition_WithInvalidAttributeId_Throws(string key, string value) - { - Assert.Throws(() => _queryParser - .Parse(CreateParameters(GetSingleton(key, value), QueryResource.AllStudies), QueryTagService.CoreQueryTags)); - } - - [Theory] - [InlineData("StudyDate", "19510910-20200220")] - public void GivenStudyDate_WithValidRangeMatch_CheckCondition(string key, string value) - { - EnsureArg.IsNotNull(value, nameof(value)); - QueryExpression queryExpression = _queryParser - .Parse(CreateParameters(GetSingleton(key, value), QueryResource.AllStudies), QueryTagService.CoreQueryTags); - var cond = queryExpression.FilterConditions.First() as DateRangeValueMatchCondition; - Assert.NotNull(cond); - Assert.Equal(DicomTag.StudyDate, cond.QueryTag.Tag); - Assert.Equal(DateTime.ParseExact(value.Split('-')[0], QueryParser.DateTagValueFormat, null), cond.Minimum); - Assert.Equal(DateTime.ParseExact(value.Split('-')[1], QueryParser.DateTagValueFormat, null), cond.Maximum); - } - - [Theory] - [InlineData("StudyDate", "-20200220")] - public void GivenStudyDate_WithEmptyMinValueInRangeMatch_CheckCondition(string key, string value) - { - EnsureArg.IsNotNull(value, nameof(value)); - QueryExpression queryExpression = _queryParser - .Parse(CreateParameters(GetSingleton(key, value), QueryResource.AllStudies), QueryTagService.CoreQueryTags); - var cond = queryExpression.FilterConditions.First() as DateRangeValueMatchCondition; - Assert.NotNull(cond); - Assert.Equal(DicomTag.StudyDate, cond.QueryTag.Tag); - Assert.Equal(DateTime.MinValue, cond.Minimum); - Assert.Equal(DateTime.ParseExact(value.Split('-')[1], QueryParser.DateTagValueFormat, null), cond.Maximum); - } - - [Theory] - [InlineData("StudyDate", "19510910-")] - public void GivenStudyDate_WithEmptyMaxValueInRangeMatch_CheckCondition(string key, string value) - { - EnsureArg.IsNotNull(value, nameof(value)); - QueryExpression queryExpression = _queryParser - .Parse(CreateParameters(GetSingleton(key, value), QueryResource.AllStudies), QueryTagService.CoreQueryTags); - var cond = queryExpression.FilterConditions.First() as DateRangeValueMatchCondition; - Assert.NotNull(cond); - Assert.Equal(DicomTag.StudyDate, cond.QueryTag.Tag); - Assert.Equal(DateTime.ParseExact(value.Split('-')[0], QueryParser.DateTagValueFormat, null), cond.Minimum); - Assert.Equal(DateTime.MaxValue, cond.Maximum); - } - - [Theory] - [InlineData("StudyDate", "2020/02/28")] - [InlineData("StudyDate", "20200230")] - [InlineData("StudyDate", "20200228-20200230")] - [InlineData("StudyDate", "20200110-20200109")] - [InlineData("StudyDate", "-")] - [InlineData("PerformedProcedureStepStartDate", "baddate")] - public void GivenDateTag_WithInvalidDate_Throw(string key, string value) - { - Assert.Throws(() => _queryParser - .Parse(CreateParameters(GetSingleton(key, value), QueryResource.AllSeries), QueryTagService.CoreQueryTags)); - } - - [Theory] - [InlineData("ReferencedRequestSequence.Requested​Procedure​ID", "Foo")] - public void GivenSequenceTag_WithMultipleTagsNotSupported_ThenThrow(string key, string value) - { - Assert.Throws(() => _queryParser - .Parse(CreateParameters(GetSingleton(key, value), QueryResource.AllSeries), QueryTagService.CoreQueryTags)); - } - - [Fact] - public void GivenStudyInstanceUID_WithUrl_CheckFilterCondition() - { - var testStudyInstanceUid = TestUidGenerator.Generate(); - QueryExpression queryExpression = _queryParser - .Parse(CreateParameters(new Dictionary(), QueryResource.AllSeries, testStudyInstanceUid), QueryTagService.CoreQueryTags); - Assert.Single(queryExpression.FilterConditions); - var cond = queryExpression.FilterConditions.First() as StringSingleValueMatchCondition; - Assert.NotNull(cond); - Assert.Equal(testStudyInstanceUid, cond.Value); - } - - [Fact] - public void GivenPatientNameFilterCondition_WithFuzzyMatchingTrue_FuzzyMatchConditionAdded() - { - QueryExpression queryExpression = _queryParser.Parse( - CreateParameters( - new Dictionary - { - { "PatientName", "CoronaPatient" }, - { "StudyDate", "20200403" }, - }, - QueryResource.AllStudies, - fuzzyMatching: true), - QueryTagService.CoreQueryTags); - - Assert.Equal(2, queryExpression.FilterConditions.Count); - - var studyDateFilterCondition = queryExpression.FilterConditions.FirstOrDefault(c => c.QueryTag.Tag == DicomTag.StudyDate) as DateSingleValueMatchCondition; - Assert.NotNull(studyDateFilterCondition); - - QueryFilterCondition patientNameCondition = queryExpression.FilterConditions.FirstOrDefault(c => c.QueryTag.Tag == DicomTag.PatientName); - Assert.NotNull(patientNameCondition); - - var fuzzyCondition = patientNameCondition as PersonNameFuzzyMatchCondition; - Assert.NotNull(fuzzyCondition); - Assert.Equal("CoronaPatient", fuzzyCondition.Value); - } - - [Fact] - public void GivenErroneousTag_WhenParse_ThenShouldBeInList() - { - DicomTag tag1 = DicomTag.PatientAge; - DicomTag tag2 = DicomTag.PatientAddress; - QueryTag[] tags = new QueryTag[] - { - new QueryTag(new ExtendedQueryTagStoreEntry(1, tag1.GetPath(), tag1.GetDefaultVR().Code, null, QueryTagLevel.Instance, ExtendedQueryTagStatus.Ready, QueryStatus.Enabled,1)), // has error - new QueryTag(new ExtendedQueryTagStoreEntry(2, tag2.GetPath(), tag2.GetDefaultVR().Code, null, QueryTagLevel.Instance, ExtendedQueryTagStatus.Ready, QueryStatus.Enabled,0)), // no error - }; - QueryExpression queryExpression = _queryParser.Parse( - CreateParameters( - new Dictionary - { - { tag1.GetFriendlyName(), "CoronaPatient" }, - { tag2.GetPath(), "20200403" }, - }, - QueryResource.AllInstances), - tags); - - Assert.Single(queryExpression.ErroneousTags); - Assert.Equal(queryExpression.ErroneousTags.First(), tag1.GetFriendlyName()); - } - - [Fact] - public void GivenDisabledTag_WhenParse_ThenShouldThrowException() - { - DicomTag tag1 = DicomTag.PatientAge; - QueryTag[] tags = new QueryTag[] - { - new QueryTag(new ExtendedQueryTagStoreEntry(1, tag1.GetPath(), tag1.GetDefaultVR().Code, null, QueryTagLevel.Instance, ExtendedQueryTagStatus.Ready, QueryStatus.Disabled,1)), // disabled - }; - var parameters = CreateParameters( - new Dictionary - { - { tag1.GetFriendlyName(), "CoronaPatient" }, - }, - QueryResource.AllInstances); - - var exp = Assert.Throws(() => _queryParser.Parse(parameters, tags)); - Assert.Equal($"Query is disabled on specified attribute '{tag1.GetFriendlyName()}'.", exp.Message); - } - - private void VerifyIncludeFieldsForValidAttributeIds(params string[] values) - { - QueryExpression queryExpression = _queryParser.Parse( - CreateParameters(new Dictionary(), QueryResource.AllStudies, includeField: values), - QueryTagService.CoreQueryTags); - - Assert.False(queryExpression.HasFilters); - Assert.False(queryExpression.IncludeFields.All); - Assert.Equal(values.Length, queryExpression.IncludeFields.DicomTags.Count); - } - - private static Dictionary GetSingleton(string key, string value) - => new Dictionary { { key, value } }; - - private static QueryParameters CreateParameters( - Dictionary filters, - QueryResource resourceType, - string studyInstanceUid = null, - string seriesInstanceUid = null, - bool fuzzyMatching = false, - string[] includeField = null) - { - return new QueryParameters - { - Filters = filters, - FuzzyMatching = fuzzyMatching, - IncludeField = includeField ?? Array.Empty(), - QueryResourceType = resourceType, - SeriesInstanceUid = seriesInstanceUid, - StudyInstanceUid = studyInstanceUid, - }; - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Query/QueryResponseBuilderTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Query/QueryResponseBuilderTests.cs deleted file mode 100644 index 187132b91f..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Query/QueryResponseBuilderTests.cs +++ /dev/null @@ -1,223 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Features.Query; -using Microsoft.Health.Dicom.Core.Features.Query.Model; -using Microsoft.Health.Dicom.Tests.Common; -using Microsoft.Health.Dicom.Tests.Common.Extensions; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Query; - -public class QueryResponseBuilderTests -{ - [Fact] - public void GivenStudyLevel_WithIncludeField_ValidReturned() - { - var includeField = new QueryIncludeField(new List { DicomTag.StudyDescription, DicomTag.IssuerOfPatientID }); - var queryTag = new QueryTag(DicomTag.PatientAge.BuildExtendedQueryTagStoreEntry(level: QueryTagLevel.Study)); - var filters = new List() - { - new StringSingleValueMatchCondition(queryTag, "35"), - }; - var query = new QueryExpression(QueryResource.AllStudies, includeField, false, 0, 0, filters, Array.Empty()); - var responseBuilder = new QueryResponseBuilder(query); - - DicomDataset responseDataset = responseBuilder.GenerateResponseDataset(GenerateTestDataSet()); - var tags = responseDataset.Select(i => i.Tag).ToList(); - - Assert.Contains(DicomTag.StudyInstanceUID, tags); // Default - Assert.Contains(DicomTag.PatientAge, tags); // Match condition - Assert.Contains(DicomTag.StudyDescription, tags); // Valid include - Assert.Contains(DicomTag.IssuerOfPatientID, tags); // non standard include - Assert.DoesNotContain(DicomTag.SeriesInstanceUID, tags); // Invalid study resource - Assert.DoesNotContain(DicomTag.SOPInstanceUID, tags); // Invalid study resource - } - - [Fact] - public void GivenStudySeriesLevel_WithIncludeField_ValidReturned() - { - var includeField = new QueryIncludeField(new List { DicomTag.StudyDescription, DicomTag.Modality }); - var queryTag = new QueryTag(DicomTag.StudyInstanceUID.BuildExtendedQueryTagStoreEntry(level: QueryTagLevel.Study)); - var filters = new List() - { - new StringSingleValueMatchCondition(queryTag, "35"), - }; - var query = new QueryExpression(QueryResource.StudySeries, includeField, false, 0, 0, filters, Array.Empty()); - var responseBuilder = new QueryResponseBuilder(query); - - DicomDataset responseDataset = responseBuilder.GenerateResponseDataset(GenerateTestDataSet()); - var tags = responseDataset.Select(i => i.Tag).ToList(); - - Assert.Contains(DicomTag.StudyInstanceUID, tags); // Valid filter - Assert.Contains(DicomTag.StudyDescription, tags); // Valid include - Assert.Contains(DicomTag.Modality, tags); // Valid include - Assert.Contains(DicomTag.SeriesInstanceUID, tags); // Valid Series resource - Assert.DoesNotContain(DicomTag.SOPInstanceUID, tags); // Invalid Series resource - } - - [Fact] - public void GivenAllSeriesLevel_WithIncludeField_ValidReturned() - { - var includeField = QueryIncludeField.AllFields; - var filters = new List(); - var query = new QueryExpression(QueryResource.AllSeries, includeField, false, 0, 0, filters, Array.Empty()); - var responseBuilder = new QueryResponseBuilder(query); - - DicomDataset responseDataset = responseBuilder.GenerateResponseDataset(GenerateTestDataSet()); - var tags = responseDataset.Select(i => i.Tag).ToList(); - - Assert.Contains(DicomTag.StudyInstanceUID, tags); // Valid study field - Assert.Contains(DicomTag.StudyDescription, tags); // Valid all study field - Assert.Contains(DicomTag.Modality, tags); // Valid series field - Assert.Contains(DicomTag.SeriesInstanceUID, tags); // Valid Series resource - Assert.DoesNotContain(DicomTag.SOPInstanceUID, tags); // Invalid Series resource - } - - [Fact] - public void GivenAllInstanceLevel_WithIncludeField_ValidReturned() - { - var includeField = QueryIncludeField.AllFields; - var filters = new List(); - var query = new QueryExpression(QueryResource.AllInstances, includeField, false, 0, 0, filters, Array.Empty()); - var responseBuilder = new QueryResponseBuilder(query); - - DicomDataset responseDataset = responseBuilder.GenerateResponseDataset(GenerateTestDataSet()); - var tags = responseDataset.Select(i => i.Tag).ToList(); - - Assert.Contains(DicomTag.StudyInstanceUID, tags); // Valid study field - Assert.Contains(DicomTag.StudyDescription, tags); // Valid all study field - Assert.Contains(DicomTag.Modality, tags); // Valid instance field - Assert.Contains(DicomTag.SeriesInstanceUID, tags); // Valid instance resource - Assert.Contains(DicomTag.SOPInstanceUID, tags); // Valid instance resource - } - - [Fact] - public void GivenStudyInstanceLevel_WithIncludeField_ValidReturned() - { - var includeField = new QueryIncludeField(new List { DicomTag.Modality }); - var filters = new List() - { - new StringSingleValueMatchCondition(new QueryTag(DicomTag.StudyInstanceUID), "35"), - }; - var query = new QueryExpression(QueryResource.StudyInstances, includeField, false, 0, 0, filters, Array.Empty()); - var responseBuilder = new QueryResponseBuilder(query); - - DicomDataset responseDataset = responseBuilder.GenerateResponseDataset(GenerateTestDataSet()); - var tags = responseDataset.Select(i => i.Tag).ToList(); - - Assert.Contains(DicomTag.StudyInstanceUID, tags); // Valid filter - Assert.DoesNotContain(DicomTag.StudyDescription, tags); // StudyInstance does not include study tags by deault - Assert.Contains(DicomTag.Modality, tags); // Valid series field - Assert.Contains(DicomTag.SeriesInstanceUID, tags); // Valid series tag - Assert.Contains(DicomTag.SOPInstanceUID, tags); // Valid instance tag - } - - [Fact] - public void GivenStudySeriesInstanceLevel_WithIncludeField_ValidReturned() - { - var includeField = new QueryIncludeField(new List()); - - var filters = new List() - { - new StringSingleValueMatchCondition(new QueryTag(DicomTag.StudyInstanceUID), "35"), - new StringSingleValueMatchCondition(new QueryTag(DicomTag.SeriesInstanceUID), "351"), - }; - var query = new QueryExpression(QueryResource.StudySeriesInstances, includeField, false, 0, 0, filters, Array.Empty()); - var responseBuilder = new QueryResponseBuilder(query); - - DicomDataset responseDataset = responseBuilder.GenerateResponseDataset(GenerateTestDataSet()); - var tags = responseDataset.Select(i => i.Tag).ToList(); - - Assert.Contains(DicomTag.StudyInstanceUID, tags); // Valid filter - Assert.DoesNotContain(DicomTag.StudyDescription, tags); // StudySeriesInstance does not include study tags by deault - Assert.DoesNotContain(DicomTag.Modality, tags); // StudySeriesInstance does not include series tags by deault - Assert.Contains(DicomTag.SeriesInstanceUID, tags); // Valid series tag - Assert.Contains(DicomTag.SOPInstanceUID, tags); // Valid instance tag - } - - [Fact] - public void GivenDefault_DefaultForV1_ValidReturned() - { - var includeField = new QueryIncludeField(new List()); - var filters = new List(); - - var query = new QueryExpression(QueryResource.AllStudies, includeField, false, 0, 0, filters, Array.Empty()); - bool useNewDefaults = false; - var responseBuilder = new QueryResponseBuilder(query, useNewDefaults); - XAssert.ContainsExactlyAll(responseBuilder.ReturnTags, QueryResponseBuilder.DefaultStudyTags); - - query = new QueryExpression(QueryResource.AllSeries, includeField, false, 0, 0, filters, Array.Empty()); - responseBuilder = new QueryResponseBuilder(query, useNewDefaults); - XAssert.ContainsExactlyAll(responseBuilder.ReturnTags, QueryResponseBuilder.DefaultStudyTags.Union(QueryResponseBuilder.DefaultSeriesTags)); - - query = new QueryExpression(QueryResource.AllInstances, includeField, false, 0, 0, filters, Array.Empty()); - responseBuilder = new QueryResponseBuilder(query, useNewDefaults); - XAssert.ContainsExactlyAll(responseBuilder.ReturnTags, QueryResponseBuilder.DefaultStudyTags.Union(QueryResponseBuilder.DefaultSeriesTags).Union(QueryResponseBuilder.DefaultInstancesTags)); - - query = new QueryExpression(QueryResource.StudySeries, includeField, false, 0, 0, filters, Array.Empty()); - responseBuilder = new QueryResponseBuilder(query, useNewDefaults); - XAssert.ContainsExactlyAll(responseBuilder.ReturnTags, QueryResponseBuilder.DefaultSeriesTags); - - query = new QueryExpression(QueryResource.StudySeriesInstances, includeField, false, 0, 0, filters, Array.Empty()); - responseBuilder = new QueryResponseBuilder(query, useNewDefaults); - XAssert.ContainsExactlyAll(responseBuilder.ReturnTags, QueryResponseBuilder.DefaultInstancesTags); - - query = new QueryExpression(QueryResource.StudyInstances, includeField, false, 0, 0, filters, Array.Empty()); - responseBuilder = new QueryResponseBuilder(query, useNewDefaults); - XAssert.ContainsExactlyAll(responseBuilder.ReturnTags, QueryResponseBuilder.DefaultSeriesTags.Union(QueryResponseBuilder.DefaultInstancesTags)); - } - - [Fact] - public void GivenAllInstance_DefaultForV2_ValidReturned() - { - var includeField = new QueryIncludeField(new List()); - var filters = new List(); - - var query = new QueryExpression(QueryResource.AllStudies, includeField, false, 0, 0, filters, Array.Empty()); - bool useNewDefaults = true; - var responseBuilder = new QueryResponseBuilder(query, useNewDefaults); - XAssert.ContainsExactlyAll(responseBuilder.ReturnTags, QueryResponseBuilder.V2DefaultStudyTags); - - query = new QueryExpression(QueryResource.AllSeries, includeField, false, 0, 0, filters, Array.Empty()); - responseBuilder = new QueryResponseBuilder(query, useNewDefaults); - XAssert.ContainsExactlyAll(responseBuilder.ReturnTags, QueryResponseBuilder.V2DefaultStudyTags.Union(QueryResponseBuilder.V2DefaultSeriesTags)); - - query = new QueryExpression(QueryResource.AllInstances, includeField, false, 0, 0, filters, Array.Empty()); - responseBuilder = new QueryResponseBuilder(query, useNewDefaults); - XAssert.ContainsExactlyAll(responseBuilder.ReturnTags, QueryResponseBuilder.V2DefaultStudyTags.Union(QueryResponseBuilder.V2DefaultSeriesTags).Union(QueryResponseBuilder.V2DefaultInstancesTags)); - - query = new QueryExpression(QueryResource.StudySeries, includeField, false, 0, 0, filters, Array.Empty()); - responseBuilder = new QueryResponseBuilder(query, useNewDefaults); - XAssert.ContainsExactlyAll(responseBuilder.ReturnTags, QueryResponseBuilder.V2DefaultSeriesTags); - - query = new QueryExpression(QueryResource.StudySeriesInstances, includeField, false, 0, 0, filters, Array.Empty()); - responseBuilder = new QueryResponseBuilder(query, useNewDefaults); - XAssert.ContainsExactlyAll(responseBuilder.ReturnTags, QueryResponseBuilder.V2DefaultInstancesTags); - - query = new QueryExpression(QueryResource.StudyInstances, includeField, false, 0, 0, filters, Array.Empty()); - responseBuilder = new QueryResponseBuilder(query, useNewDefaults); - XAssert.ContainsExactlyAll(responseBuilder.ReturnTags, QueryResponseBuilder.V2DefaultSeriesTags.Union(QueryResponseBuilder.V2DefaultInstancesTags)); - } - - private static DicomDataset GenerateTestDataSet() - { - return new DicomDataset() - { - { DicomTag.StudyInstanceUID, TestUidGenerator.Generate() }, - { DicomTag.SeriesInstanceUID, TestUidGenerator.Generate() }, - { DicomTag.SOPInstanceUID, TestUidGenerator.Generate() }, - { DicomTag.PatientAge, "035Y" }, - { DicomTag.StudyDescription, "CT scan" }, - { DicomTag.IssuerOfPatientID, "Homeland" }, - { DicomTag.Modality, "CT" }, - }; - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Query/QueryServiceTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Query/QueryServiceTests.cs deleted file mode 100644 index 7f7907c7d7..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Query/QueryServiceTests.cs +++ /dev/null @@ -1,369 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Data; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using FellowOakDicom; -using Microsoft.Extensions.Logging; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Context; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Core.Features.Query; -using Microsoft.Health.Dicom.Core.Features.Query.Model; -using Microsoft.Health.Dicom.Tests.Common; -using Microsoft.Health.Dicom.Tests.Common.Comparers; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Query; - -public class QueryServiceTests -{ - private readonly QueryService _queryService; - private readonly IQueryParser _queryParser; - private readonly IQueryStore _queryStore; - private readonly IMetadataStore _metadataStore; - private readonly IQueryTagService _queryTagService; - private readonly IDicomRequestContextAccessor _contextAccessor; - - public QueryServiceTests() - { - _queryParser = Substitute.For>(); - _queryStore = Substitute.For(); - _metadataStore = Substitute.For(); - _queryTagService = Substitute.For(); - _contextAccessor = Substitute.For(); - _contextAccessor.RequestContext.DataPartition = Partition.Default; - - _queryService = new QueryService( - _queryParser, - _queryStore, - _metadataStore, - _queryTagService, - _contextAccessor, - Substitute.For>()); - } - - [Theory] - [InlineData(QueryResource.StudySeries, "123.001", Skip = "Enable once UID validation rejects leading zeroes.")] - [InlineData(QueryResource.StudyInstances, "abc.1234")] - public Task GivenQidoQuery_WithInvalidStudyInstanceUid_ThrowsValidationException(QueryResource resourceType, string studyInstanceUid) - { - var parameters = new QueryParameters - { - Filters = new Dictionary(), - QueryResourceType = resourceType, - StudyInstanceUid = studyInstanceUid - }; - - return Assert.ThrowsAsync(() => _queryService.QueryAsync(parameters, CancellationToken.None)); - } - - [Theory] - [InlineData(QueryResource.StudySeriesInstances, "123.111", "1234.001", Skip = "Enable once UID validation rejects leading zeroes.")] - [InlineData(QueryResource.StudySeriesInstances, "123.abc", "1234.001")] - public Task GivenQidoQuery_WithInvalidStudySeriesUid_ThrowsValidationException(QueryResource resourceType, string studyInstanceUid, string seriesInstanceUid) - { - var parameters = new QueryParameters - { - Filters = new Dictionary(), - QueryResourceType = resourceType, - SeriesInstanceUid = seriesInstanceUid, - StudyInstanceUid = studyInstanceUid, - }; - - return Assert.ThrowsAsync(() => _queryService.QueryAsync(parameters, CancellationToken.None)); - } - - [Theory] - [InlineData(QueryResource.AllInstances)] - [InlineData(QueryResource.StudyInstances)] - [InlineData(QueryResource.StudySeriesInstances)] - public async Task GivenRequestForInstances_WhenRetrievingQueriableExtendedQueryTags_ReturnsAllTags(QueryResource resourceType) - { - _queryParser.Parse(default, default).ReturnsForAnyArgs(new QueryExpression(default, default, default, default, default, Array.Empty(), Array.Empty())); - var parameters = new QueryParameters - { - Filters = new Dictionary(), - QueryResourceType = resourceType, - SeriesInstanceUid = TestUidGenerator.Generate(), - StudyInstanceUid = TestUidGenerator.Generate(), - }; - List storeEntries = new List() - { - new ExtendedQueryTagStoreEntry(1, "00741000", "CS", null, QueryTagLevel.Instance, ExtendedQueryTagStatus.Ready, QueryStatus.Enabled, 0), - new ExtendedQueryTagStoreEntry(2, "0040A121", "DA", null, QueryTagLevel.Series, ExtendedQueryTagStatus.Ready, QueryStatus.Enabled, 0), - new ExtendedQueryTagStoreEntry(3, "00101005", "PN", null, QueryTagLevel.Study, ExtendedQueryTagStatus.Ready, QueryStatus.Enabled, 0), - }; - - var list = QueryTagService.CoreQueryTags.Concat(storeEntries.Select(item => new QueryTag(item))).ToList(); - _queryTagService.GetQueryTagsAsync().ReturnsForAnyArgs(list); - _queryStore.QueryAsync(Arg.Any(), Arg.Any(), Arg.Any()).ReturnsForAnyArgs(new QueryResult(new List())); - await _queryService.QueryAsync(parameters, CancellationToken.None); - - _queryParser.Received().Parse(parameters, Arg.Do>(x => Assert.Equal(x, list, QueryTagComparer.Default))); - } - - [Theory] - [InlineData(QueryResource.AllSeries)] - [InlineData(QueryResource.StudySeries)] - public async Task GivenRequestForSeries_WhenRetrievingQueriableExtendedQueryTags_ReturnsSeriesAndStudyTags(QueryResource resourceType) - { - _queryParser.Parse(default, default).ReturnsForAnyArgs(new QueryExpression(default, default, default, default, default, Array.Empty(), Array.Empty())); - var parameters = new QueryParameters - { - Filters = new Dictionary(), - QueryResourceType = resourceType, - SeriesInstanceUid = TestUidGenerator.Generate(), - StudyInstanceUid = TestUidGenerator.Generate(), - }; - List storeEntries = new List() - { - new ExtendedQueryTagStoreEntry(1, "00741000", "CS", null, QueryTagLevel.Instance, ExtendedQueryTagStatus.Ready, QueryStatus.Enabled, 0), - new ExtendedQueryTagStoreEntry(2, "0040A121", "DA", null, QueryTagLevel.Series, ExtendedQueryTagStatus.Ready, QueryStatus.Enabled, 0), - new ExtendedQueryTagStoreEntry(3, "00101005", "PN", null, QueryTagLevel.Study, ExtendedQueryTagStatus.Ready, QueryStatus.Enabled, 0), - }; - - var list = QueryTagService.CoreQueryTags.Concat(storeEntries.Select(item => new QueryTag(item))).ToList(); - _queryStore.QueryAsync(Arg.Any(), Arg.Any(), Arg.Any()).ReturnsForAnyArgs(new QueryResult(new List())); - await _queryService.QueryAsync(parameters, CancellationToken.None); - - _queryParser.Received().Parse(parameters, Arg.Do>(x => Assert.Equal(x, list, QueryTagComparer.Default))); - } - - [Theory] - [InlineData(QueryResource.AllStudies)] - public async Task GivenRequestForStudies_WhenRetrievingQueriableExtendedQueryTags_ReturnsStudyTags(QueryResource resourceType) - { - _queryParser.Parse(default, default).ReturnsForAnyArgs(new QueryExpression(default, default, default, default, default, Array.Empty(), Array.Empty())); - var parameters = new QueryParameters - { - Filters = new Dictionary(), - QueryResourceType = resourceType, - SeriesInstanceUid = TestUidGenerator.Generate(), - StudyInstanceUid = TestUidGenerator.Generate(), - }; - - List storeEntries = new List() - { - new ExtendedQueryTagStoreEntry(1, "00741000", "CS", null, QueryTagLevel.Instance, ExtendedQueryTagStatus.Ready, QueryStatus.Enabled, 0), - new ExtendedQueryTagStoreEntry(2, "0040A121", "DA", null, QueryTagLevel.Series, ExtendedQueryTagStatus.Ready, QueryStatus.Enabled, 0), - new ExtendedQueryTagStoreEntry(3, "00101005", "PN", null, QueryTagLevel.Study, ExtendedQueryTagStatus.Ready, QueryStatus.Enabled, 0), - }; - - var list = QueryTagService.CoreQueryTags.Concat(storeEntries.Select(item => new QueryTag(item))).ToList(); - _queryStore.QueryAsync(Arg.Any(), Arg.Any(), Arg.Any()).ReturnsForAnyArgs(new QueryResult(new List())); - await _queryService.QueryAsync(parameters, CancellationToken.None); - - _queryParser.Received().Parse(parameters, Arg.Do>(x => Assert.Equal(x, list, QueryTagComparer.Default))); - } - - [Fact] - public async Task GivenRequest_WhenV2DefaultStudyExpected_OnlyStudyResultPathIsCalled() - { - VersionedInstanceIdentifier identifier = new VersionedInstanceIdentifier(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 1); - _queryParser.Parse(default, default).ReturnsForAnyArgs( - new QueryExpression(QueryResource.AllStudies, new QueryIncludeField(new List()), default, 0, 0, Array.Empty(), Array.Empty())); - _queryStore.QueryAsync(Arg.Any(), Arg.Any(), Arg.Any()).ReturnsForAnyArgs(new QueryResult(new List() { identifier })); - _contextAccessor.RequestContext.Version = 2; - - await _queryService.QueryAsync(new QueryParameters(), CancellationToken.None); - - await _queryStore.Received().GetStudyResultAsync(Arg.Any(), Arg.Any>(), Arg.Any()); - await _queryStore.DidNotReceive().GetSeriesResultAsync(Arg.Any(), Arg.Any>(), Arg.Any()); - await _metadataStore.DidNotReceive().GetInstanceMetadataAsync(Arg.Any(), Arg.Any()); - } - - [Fact] - public async Task GivenRequest_WhenDefaultStudySeriesExpected_OnlyStudySeriesResultPathIsCalled() - { - VersionedInstanceIdentifier identifier = new VersionedInstanceIdentifier(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 1); - _queryParser.Parse(default, default).ReturnsForAnyArgs( - new QueryExpression(QueryResource.AllSeries, new QueryIncludeField(new List()), default, 0, 0, Array.Empty(), Array.Empty())); - _queryStore.QueryAsync(Arg.Any(), Arg.Any(), Arg.Any()).ReturnsForAnyArgs(new QueryResult(new List() { identifier })); - _contextAccessor.RequestContext.Version = 2; - var studyResults = GenerateStudyResults(identifier.StudyInstanceUid); - var seriesResults = GenerateSeriesResults(identifier.StudyInstanceUid, identifier.SeriesInstanceUid, TestUidGenerator.Generate()); - _queryStore.GetStudyResultAsync(Arg.Any(), Arg.Any>(), Arg.Any()).Returns(studyResults); - _queryStore.GetSeriesResultAsync(Arg.Any(), Arg.Any>(), Arg.Any()).Returns(seriesResults); - - var response = await _queryService.QueryAsync(new QueryParameters(), CancellationToken.None); - - await _queryStore.Received().GetStudyResultAsync(Arg.Any(), Arg.Any>(), Arg.Any()); - await _queryStore.Received().GetSeriesResultAsync(Arg.Any(), Arg.Any>(), Arg.Any()); - await _metadataStore.DidNotReceive().GetInstanceMetadataAsync(Arg.Any(), Arg.Any()); - Assert.Equal(2, response.ResponseDataset.Count()); - ValidationResponse(response.ResponseDataset, studyResults.Single(), seriesResults); - } - - [Fact] - public async Task GivenRequest_WhenDefaultSeriesExpected_OnlySeriesResultPathIsCalled() - { - VersionedInstanceIdentifier identifier = new VersionedInstanceIdentifier(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 1); - _queryParser.Parse(default, default).ReturnsForAnyArgs( - new QueryExpression(QueryResource.StudySeries, new QueryIncludeField(new List()), default, 0, 0, Array.Empty(), Array.Empty())); - _queryStore.QueryAsync(Arg.Any(), Arg.Any(), Arg.Any()).ReturnsForAnyArgs(new QueryResult(new List() { identifier })); - _contextAccessor.RequestContext.Version = 2; - - await _queryService.QueryAsync(new QueryParameters(), CancellationToken.None); - - await _queryStore.DidNotReceive().GetStudyResultAsync(Arg.Any(), Arg.Any>(), Arg.Any()); - await _queryStore.Received().GetSeriesResultAsync(Arg.Any(), Arg.Any>(), Arg.Any()); - await _metadataStore.DidNotReceive().GetInstanceMetadataAsync(Arg.Any(), Arg.Any()); - } - - [Fact] - public async Task GivenRequest_WhenAllRequests_MetadataPathCalled() - { - VersionedInstanceIdentifier identifier = new VersionedInstanceIdentifier(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 1); - _queryParser.Parse(default, default).ReturnsForAnyArgs( - new QueryExpression(QueryResource.AllStudies, QueryIncludeField.AllFields, default, 0, 0, Array.Empty(), Array.Empty())); - _queryStore.QueryAsync(Arg.Any(), Arg.Any(), Arg.Any()).ReturnsForAnyArgs(new QueryResult(new List() { identifier })); - _contextAccessor.RequestContext.Version = 2; - - await _queryService.QueryAsync(new QueryParameters(), CancellationToken.None); - - await _queryStore.DidNotReceive().GetStudyResultAsync(Arg.Any(), Arg.Any>(), Arg.Any()); - await _queryStore.DidNotReceive().GetSeriesResultAsync(Arg.Any(), Arg.Any>(), Arg.Any()); - await _metadataStore.Received().GetInstanceMetadataAsync(Arg.Any(), Arg.Any()); - } - - [Fact] - public async Task GivenRequest_WhenAllRequestsAndComputedRequested_MetadataPathAndStudyResultCalled() - { - var includeFields = new QueryIncludeField(new List() { DicomTag.PatientAdditionalPosition, DicomTag.ProposedStudySequence, DicomTag.ModalitiesInStudy }); - VersionedInstanceIdentifier identifier = new VersionedInstanceIdentifier(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 1); - _queryParser.Parse(default, default).ReturnsForAnyArgs( - new QueryExpression(QueryResource.AllStudies, includeFields, default, 0, 0, Array.Empty(), Array.Empty())); - _queryStore.QueryAsync(Arg.Any(), Arg.Any(), Arg.Any()).ReturnsForAnyArgs(new QueryResult(new List() { identifier })); - _contextAccessor.RequestContext.Version = 2; - var studyResults = GenerateStudyResults(identifier.StudyInstanceUid); - var metadataResult = GenerateMetadataStoreResponse(identifier); - _queryStore.GetStudyResultAsync(Arg.Any(), Arg.Any>(), Arg.Any()).Returns(studyResults); - _metadataStore.GetInstanceMetadataAsync(Arg.Any(), Arg.Any()).Returns(metadataResult); - - var response = await _queryService.QueryAsync(new QueryParameters(), CancellationToken.None); - - await _queryStore.Received().GetStudyResultAsync(Arg.Any(), Arg.Any>(), Arg.Any()); - await _queryStore.DidNotReceive().GetSeriesResultAsync(Arg.Any(), Arg.Any>(), Arg.Any()); - await _metadataStore.Received().GetInstanceMetadataAsync(Arg.Any(), Arg.Any()); - Assert.Single(response.ResponseDataset); - ValidationResponse(response.ResponseDataset.Single(), studyResults.Single(), metadataResult); - } - - [Fact] - public async Task GivenRequest_WhenDefaultSeriesWithComputedCalled_StudyPathIsNotCalled() - { - var includeFields = new QueryIncludeField(new List() { DicomTag.PatientAdditionalPosition, DicomTag.NumberOfSeriesRelatedInstances, DicomTag.Modality }); - VersionedInstanceIdentifier identifier = new VersionedInstanceIdentifier(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 1); - _queryParser.Parse(default, default).ReturnsForAnyArgs( - new QueryExpression(QueryResource.AllSeries, includeFields, default, 0, 0, Array.Empty(), Array.Empty())); - _queryStore.QueryAsync(Arg.Any(), Arg.Any(), Arg.Any()).ReturnsForAnyArgs(new QueryResult(new List() { identifier })); - _contextAccessor.RequestContext.Version = 2; - - await _queryService.QueryAsync(new QueryParameters(), CancellationToken.None); - - await _queryStore.DidNotReceive().GetStudyResultAsync(Arg.Any(), Arg.Any>(), Arg.Any()); - await _queryStore.Received().GetSeriesResultAsync(Arg.Any(), Arg.Any>(), Arg.Any()); - await _metadataStore.Received().GetInstanceMetadataAsync(Arg.Any(), Arg.Any()); - } - - [Fact] - public async Task GivenRequest_WhenStudiesDeletedFromDBAfterQueryResultBeforeComputeCalled_RequestShouldSucceed() - { - var includeFields = new QueryIncludeField(new List() { DicomTag.PatientAdditionalPosition, DicomTag.ProposedStudySequence, DicomTag.ModalitiesInStudy }); - VersionedInstanceIdentifier identifier = new VersionedInstanceIdentifier(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 1); - _queryParser.Parse(default, default).ReturnsForAnyArgs( - new QueryExpression(QueryResource.AllStudies, includeFields, default, 0, 0, Array.Empty(), Array.Empty())); - _queryStore.QueryAsync(Arg.Any(), Arg.Any(), Arg.Any()).ReturnsForAnyArgs(new QueryResult(new List() { identifier })); - _contextAccessor.RequestContext.Version = 2; - var studyResults = new List(); - var metadataResult = GenerateMetadataStoreResponse(identifier); - _queryStore.GetStudyResultAsync(Arg.Any(), Arg.Any>(), Arg.Any()).Returns(studyResults); - _metadataStore.GetInstanceMetadataAsync(Arg.Any(), Arg.Any()).Returns(metadataResult); - - var response = await _queryService.QueryAsync(new QueryParameters(), CancellationToken.None); - - await _queryStore.Received().GetStudyResultAsync(Arg.Any(), Arg.Any>(), Arg.Any()); - await _queryStore.DidNotReceive().GetSeriesResultAsync(Arg.Any(), Arg.Any>(), Arg.Any()); - await _metadataStore.Received().GetInstanceMetadataAsync(Arg.Any(), Arg.Any()); - Assert.Empty(response.ResponseDataset); - } - - private static IReadOnlyCollection GenerateStudyResults(string studyInstanceUid) - { - var studyResult = new StudyResult() - { - StudyInstanceUid = studyInstanceUid, - StudyDescription = "test", - PatientId = "1234", - ModalitiesInStudy = new[] { "CT", "MR" } - }; - return new List() { studyResult }; - } - - private static IReadOnlyCollection GenerateSeriesResults(string studyInstanceUid, params string[] seriesInstanceUids) - { - var seriesResults = new List(); - - foreach (string seUid in seriesInstanceUids) - { - var seriesResult = new SeriesResult() - { - StudyInstanceUid = studyInstanceUid, - SeriesInstanceUid = seUid, - Modality = "CT" - }; - seriesResults.Add(seriesResult); - } - return seriesResults; - } - - private static void ValidationResponse(IEnumerable responseDataset, StudyResult studyResult, IReadOnlyCollection seriesResults) - { - Dictionary keyValues = new Dictionary(); - foreach (SeriesResult result in seriesResults) - { - var ds = new DicomDataset(result.DicomDataset); - ds.AddOrUpdate(studyResult.DicomDataset); - ds.Remove(DicomTag.NumberOfSeriesRelatedInstances); - ds.Remove(DicomTag.NumberOfStudyRelatedInstances); - ds.Remove(DicomTag.ModalitiesInStudy); - keyValues.Add(result.SeriesInstanceUid, ds); - } - - foreach (DicomDataset items in responseDataset) - { - Assert.Equal(keyValues[items.GetSingleValue(DicomTag.SeriesInstanceUID)], items); - } - } - - private static void ValidationResponse(DicomDataset responseDataset, StudyResult studyResult, DicomDataset metadataResult) - { - var ds = new DicomDataset(studyResult.DicomDataset); - ds.AddOrUpdate(metadataResult); - ds.Remove(DicomTag.NumberOfStudyRelatedInstances); - - Assert.Equal(ds, responseDataset); - } - - private static DicomDataset GenerateMetadataStoreResponse(VersionedInstanceIdentifier identifier) - { - return new DicomDataset() - { - { DicomTag.StudyInstanceUID, identifier.StudyInstanceUid }, - { DicomTag.PatientAdditionalPosition, "foobar" } - }; - } - - -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Query/ResultTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Query/ResultTests.cs deleted file mode 100644 index 1ab80298ae..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Query/ResultTests.cs +++ /dev/null @@ -1,39 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - - -using System.Linq; -using Microsoft.Health.Dicom.Core.Features.Query.Model; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Query; - -public class ResultsTests -{ - - [Fact] - public void GivenStudyResultsWithNullValues_DicomDatasetConversion_Works() - { - var StudyResult = new StudyResult() - { - StudyInstanceUid = "1.2.3", - NumberofStudyRelatedInstances = 1, - ModalitiesInStudy = new string[] { }, - }; - Assert.True(StudyResult.DicomDataset.Count() == 2); - } - - [Fact] - public void GivenSeriesResultsWithNullValues_DicomDatasetConversion_Works() - { - var StudyResult = new SeriesResult() - { - StudyInstanceUid = "1.2.3", - SeriesInstanceUid = "1.2.3.4", - NumberOfSeriesRelatedInstances = 1, - }; - Assert.True(StudyResult.DicomDataset.Count() == 3); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Resources/Retrieve/LazyTransformReadOnlyStreamTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Resources/Retrieve/LazyTransformReadOnlyStreamTests.cs deleted file mode 100644 index 439e37ed50..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Resources/Retrieve/LazyTransformReadOnlyStreamTests.cs +++ /dev/null @@ -1,191 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.IO; -using System.Linq; -using Microsoft.Health.Dicom.Core.Features.Retrieve; -using Microsoft.IO; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Resources.Retrieve; - -public class LazyTransformReadOnlyStreamTests -{ - private static readonly RecyclableMemoryStreamManager RecyclableMemoryStreamManager = new RecyclableMemoryStreamManager(); - private static readonly byte[] TestData = new byte[] { 1, 2, 3, 4 }; - - [Fact] - public void GivenLazyTransformStream_WhenConstructedWithInvalidArguments_ArgumentExceptionThrown() - { - Assert.Throws(() => new LazyTransformReadOnlyStream(string.Empty, null)); - } - - [Fact] - public void GivenLazyTransformStream_WhenExecutingWriteMethods_NotSupportedExceptionThrown() - { - using (var lazyTransformStream = new LazyTransformReadOnlyStream(new byte[0], ReverseByteArray)) - { - Assert.Throws(() => lazyTransformStream.SetLength(1)); - Assert.Throws(() => lazyTransformStream.Write(new byte[0], 1, 0)); - } - } - - [Fact] - public void GivenLazyTransformStream_WhenConstructed_CanReadSeekButCannotWrite() - { - Assert.Throws(() => new LazyTransformReadOnlyStream(string.Empty, null)); - - using (var lazyTransformStream = new LazyTransformReadOnlyStream(new byte[0], ReverseByteArray)) - { - Assert.True(lazyTransformStream.CanRead); - Assert.True(lazyTransformStream.CanSeek); - Assert.False(lazyTransformStream.CanWrite); - } - } - - [Fact] - public void GivenLazyTransformStream_WhenSeeking_IsSetToCorrectStreamPosition() - { - using (var lazyTransformStream = new LazyTransformReadOnlyStream(TestData, DoubleByteArray)) - { - Assert.True(lazyTransformStream.CanSeek); - Assert.Equal(TestData.Length * 2, lazyTransformStream.Length); - Assert.Equal(0, lazyTransformStream.Position); - - lazyTransformStream.Seek(2, SeekOrigin.Current); - Assert.Equal(2, lazyTransformStream.Position); - lazyTransformStream.Seek(lazyTransformStream.Length - 1, SeekOrigin.Begin); - Assert.Equal(lazyTransformStream.Length - 1, lazyTransformStream.Position); - lazyTransformStream.Seek(0, SeekOrigin.Begin); - Assert.Equal(0, lazyTransformStream.Position); - lazyTransformStream.Seek(-1, SeekOrigin.End); - Assert.Equal(lazyTransformStream.Length - 1, lazyTransformStream.Position); - } - } - - [Fact] - public void GivenReverseStreamFunction_WhenUsingLazyTransformStream_StreamIsReversed() - { - using (var lazyTransform = new LazyTransformReadOnlyStream(TestData, ReverseByteArray)) - { - Assert.Equal(TestData.Length, lazyTransform.Length); - - var resultBuffer = new byte[TestData.Length]; - Assert.Equal(TestData.Length, lazyTransform.Read(resultBuffer, 0, TestData.Length)); - - for (var i = 0; i < TestData.Length; i++) - { - Assert.Equal(TestData[i], resultBuffer[TestData.Length - 1 - i]); - } - } - } - - [Fact] - public void GivenLazyTransformStream_WhenTransformingAnInputStream_InputStreamIsNotReadUntilLazyStreamIsRead() - { - using (var inputStream = RecyclableMemoryStreamManager.GetStream("GivenLazyTransformStream_WhenTransformingAnInputStream_InputStreamIsNotReadUntilLazyStreamIsRead.TestData", TestData, 0, TestData.Length)) - using (var lazyTransform = new LazyTransformReadOnlyStream(inputStream, ReadAndCreateNewStream)) - { - Assert.Equal(0, inputStream.Position); - Assert.Equal(TestData.Length, lazyTransform.Length); - Assert.Equal(TestData.Length, inputStream.Position); - } - } - - [Fact] - public void GivenLazyTransformStreamWithByteArray_WhenDisposing_IsDisposedCorrectly() - { - GCWatch gcWatch = GetGCWatch(TestData, ReverseByteArray); - Assert.True(gcWatch.IsAlive); - } - - [Fact] - public void GivenLazyTransformStreamWithStream_WhenDisposing_IsDisposedCorrectly() - { - var inputStream = RecyclableMemoryStreamManager.GetStream("GivenLazyTransformStreamWithStream_WhenDisposing_IsDisposedCorrectly.TestData", TestData, 0, TestData.Length); - GCWatch gcWatch = GetGCWatch(inputStream, ReadAndCreateNewStream); - inputStream.Dispose(); - inputStream = null; - Assert.True(gcWatch.IsAlive); - } - - [Fact] - public void GivenLazyTransformStream_WhenTransformedDataHasDifferentLength_ReturnsTheCorrectStreamLength() - { - using (var lazyTransformStream = new LazyTransformReadOnlyStream(TestData, DoubleByteArray)) - { - Assert.Equal(TestData.Length * 2, lazyTransformStream.Length); - - var readBuffer = new byte[lazyTransformStream.Length]; - lazyTransformStream.Read(readBuffer, 0, readBuffer.Length); - Assert.Equal(readBuffer.Length, lazyTransformStream.Position); - } - } - - private static GCWatch GetGCWatch(T inputData, Func transformFunction) - { - var lazyTransform = new LazyTransformReadOnlyStream(inputData, transformFunction); - var result = new GCWatch(lazyTransform); - - // Read the entire stream - var outputBuffer = new byte[lazyTransform.Length]; - lazyTransform.Read(outputBuffer, 0, outputBuffer.Length); - - return result; - } - - private static Stream ReadAndCreateNewStream(Stream stream) - { - var resultBuffer = new byte[stream.Length]; - stream.Read(resultBuffer, 0, resultBuffer.Length); - return RecyclableMemoryStreamManager.GetStream("ReadAndCreateNewStream.resultBuffer", resultBuffer, 0, resultBuffer.Length); - } - - private static Stream ReverseByteArray(byte[] input) - { - byte[] reversedInput = input.Reverse().ToArray(); - return RecyclableMemoryStreamManager.GetStream("ReverseByteArray.reversedInput", reversedInput, 0, reversedInput.Length); - } - - private static Stream DoubleByteArray(byte[] input) - { - byte[] resultBuffer = new byte[input.Length * 2]; - for (var i = 0; i < input.Length; i++) - { - resultBuffer[i * 2] = input[i]; - resultBuffer[(i * 2) + 1] = input[i]; - } - - return RecyclableMemoryStreamManager.GetStream("DoubleByteArray.resultBuffer", resultBuffer, 0, resultBuffer.Length); - } - - private class GCWatch - { - private readonly WeakReference _weakReference; - - public GCWatch(object value) - { - _weakReference = new WeakReference(value); - } - - public bool IsAlive - { - get - { - if (!_weakReference.IsAlive) - { - return true; - } - - GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true); - GC.WaitForPendingFinalizers(); - GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true); - - return !_weakReference.IsAlive; - } - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Retrieve/AcceptHeaderDescriptorTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Retrieve/AcceptHeaderDescriptorTests.cs deleted file mode 100644 index 279d898f37..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Retrieve/AcceptHeaderDescriptorTests.cs +++ /dev/null @@ -1,222 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Health.Dicom.Core.Features.Retrieve; -using Microsoft.Health.Dicom.Core.Messages; -using Microsoft.Health.Dicom.Core.Web; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Retrieve; - -public class AcceptHeaderDescriptorTests -{ - private static readonly AcceptHeaderDescriptor ValidStudyAcceptHeaderDescriptor = AcceptHeaderHandler - .AcceptableDescriptors[ResourceType.Study] - .First(); - - [Fact] - public void - GivenTransferSyntaxIsNotMandatory_WhenConstructingAcceptHeaderDescriptorWithoutTransferSyntaxDefault_ShouldThrowException() - { - Assert.Throws(() => new AcceptHeaderDescriptor( - payloadType: PayloadTypes.SinglePart, - mediaType: KnownContentTypes.ApplicationDicom, - isTransferSyntaxMandatory: false, - transferSyntaxWhenMissing: string.Empty, - acceptableTransferSyntaxes: new HashSet()) - ); - } - - [Fact] - public void - GivenTransferSyntaxIsMandatory_WhenConstructAcceptHeaderDescriptorWithoutTransferSyntaxDefault_ShouldSucceed() - { - var _ = new AcceptHeaderDescriptor( - payloadType: PayloadTypes.SinglePart, - mediaType: KnownContentTypes.ApplicationDicom, - isTransferSyntaxMandatory: true, - transferSyntaxWhenMissing: string.Empty, - acceptableTransferSyntaxes: new HashSet() - ); - } - - [Fact] - public void GivenValidParameters_WhenUsingConstructor_ThenAllPropertiesAssigned() - { - PayloadTypes expectedPayloadType = PayloadTypes.MultipartRelated; - string expectedMediaType = KnownContentTypes.ApplicationDicom; - bool expectedIsTransferSyntaxMandatory = false; - string expectedTransferSyntaxWhenMissing = DicomTransferSyntaxUids.ExplicitVRLittleEndian; - ISet expectedAcceptableTransferSyntaxes = new HashSet() { expectedTransferSyntaxWhenMissing }; - - AcceptHeaderDescriptor descriptor = new AcceptHeaderDescriptor( - expectedPayloadType, - expectedMediaType, - expectedIsTransferSyntaxMandatory, - expectedTransferSyntaxWhenMissing, - expectedAcceptableTransferSyntaxes - ); - - Assert.Equal(expectedPayloadType, descriptor.PayloadType); - Assert.Equal(expectedMediaType, descriptor.MediaType); - Assert.Equal(expectedIsTransferSyntaxMandatory, descriptor.IsTransferSyntaxMandatory); - Assert.Equal(expectedTransferSyntaxWhenMissing, descriptor.TransferSyntaxWhenMissing); - Assert.Equal(expectedAcceptableTransferSyntaxes, descriptor.AcceptableTransferSyntaxes); - } - - - [Fact] - public void GivenUnsupportedMediaType_ThenIsNotAcceptable() - { - Assert.False(ValidStudyAcceptHeaderDescriptor.IsAcceptable( - new( - "unsupportedMediaType", - ValidStudyAcceptHeaderDescriptor.PayloadType, - ValidStudyAcceptHeaderDescriptor.AcceptableTransferSyntaxes.First()) - ) - ); - } - - [Fact] - public void GivenUnsupportedPayloadType_ThenIsNotAcceptable() - { - Assert.NotEqual(PayloadTypes.None, ValidStudyAcceptHeaderDescriptor.PayloadType); - Assert.False(ValidStudyAcceptHeaderDescriptor.IsAcceptable( - new( - ValidStudyAcceptHeaderDescriptor.MediaType, - PayloadTypes.None, - ValidStudyAcceptHeaderDescriptor.AcceptableTransferSyntaxes.First()) - ) - ); - } - - [Fact] - public void GivenAcceptHeaderWithSupportedParameters_ThenIsAcceptable() - { - Assert.True(ValidStudyAcceptHeaderDescriptor.IsAcceptable( - new( - ValidStudyAcceptHeaderDescriptor.MediaType, - ValidStudyAcceptHeaderDescriptor.PayloadType, - ValidStudyAcceptHeaderDescriptor.AcceptableTransferSyntaxes.First()) - ) - ); - } - - - [Fact] - public void GivenUnsupportedTransferSyntax_ThenIsNotAcceptable() - { - Assert.False(ValidStudyAcceptHeaderDescriptor.IsAcceptable( - new( - ValidStudyAcceptHeaderDescriptor.MediaType, - ValidStudyAcceptHeaderDescriptor.PayloadType, - "unacceptableTransferSyntax") - ) - ); - } - - [Fact] - public void - GivenAcceptHeaderWithoutTransferSyntax_WhenTransferSyntaxIsMandatoryAndNoDefaultOnDescriptor_ThenIsNotAcceptable() - { - AcceptHeaderDescriptor descriptor = new AcceptHeaderDescriptor( - payloadType: ValidStudyAcceptHeaderDescriptor.PayloadType, - mediaType: ValidStudyAcceptHeaderDescriptor.MediaType, - isTransferSyntaxMandatory: true, - transferSyntaxWhenMissing: null, - acceptableTransferSyntaxes: ValidStudyAcceptHeaderDescriptor.AcceptableTransferSyntaxes); - - Assert.True(descriptor.IsTransferSyntaxMandatory); - Assert.Null(descriptor.TransferSyntaxWhenMissing); - - Assert.False(descriptor.IsAcceptable( - new( - ValidStudyAcceptHeaderDescriptor.MediaType, - ValidStudyAcceptHeaderDescriptor.PayloadType, - transferSyntax: null) - ) - ); - } - - [Fact] - public void - GivenAcceptHeaderWithoutTransferSyntax_WhenTransferSyntaxNotMandatoryAndDefaultOnDescriptor_ThenIsAcceptable() - { - Assert.False(ValidStudyAcceptHeaderDescriptor.IsTransferSyntaxMandatory); - Assert.NotNull(ValidStudyAcceptHeaderDescriptor.TransferSyntaxWhenMissing); - - Assert.True(ValidStudyAcceptHeaderDescriptor.IsAcceptable( - new( - ValidStudyAcceptHeaderDescriptor.MediaType, - ValidStudyAcceptHeaderDescriptor.PayloadType, - transferSyntax: null) - ) - ); - } - - [Fact] - public void - GivenAcceptHeaderWithoutTransferSyntax_WhenTransferSyntaxNotMandatoryAndDefaultOnDescriptor_GetTransferSyntax_ThenDefaultSyntaxReturned() - { - Assert.False(ValidStudyAcceptHeaderDescriptor.IsTransferSyntaxMandatory); - Assert.NotNull(ValidStudyAcceptHeaderDescriptor.TransferSyntaxWhenMissing); - - Assert.Equal( - ValidStudyAcceptHeaderDescriptor.TransferSyntaxWhenMissing, - ValidStudyAcceptHeaderDescriptor.GetTransferSyntax( - new( - ValidStudyAcceptHeaderDescriptor.MediaType, - ValidStudyAcceptHeaderDescriptor.PayloadType, - transferSyntax: null) - ) - ); - } - - [Fact] - public void - GivenAcceptHeaderWithTransferSyntax_WhenTransferSyntaxMandatory_GetTransferSyntax_ThenAcceptHeaderTransferSyntaxReturned() - { - Assert.False(ValidStudyAcceptHeaderDescriptor.IsTransferSyntaxMandatory); - Assert.NotNull(ValidStudyAcceptHeaderDescriptor.TransferSyntaxWhenMissing); - Assert.NotEqual(DicomTransferSyntaxUids.Original, ValidStudyAcceptHeaderDescriptor.TransferSyntaxWhenMissing); - - Assert.Equal( - DicomTransferSyntaxUids.Original, - ValidStudyAcceptHeaderDescriptor.GetTransferSyntax( - new( - ValidStudyAcceptHeaderDescriptor.MediaType, - ValidStudyAcceptHeaderDescriptor.PayloadType, - transferSyntax: DicomTransferSyntaxUids.Original) - ) - ); - } - - [Fact] - public void - GivenAcceptHeaderWithoutTransferSyntax_WhenTransferSyntaxMandatory_GetTransferSyntax_ThenAcceptHeaderEmptyTransferSyntaxReturned() - { - AcceptHeaderDescriptor descriptor = new AcceptHeaderDescriptor( - payloadType: ValidStudyAcceptHeaderDescriptor.PayloadType, - mediaType: ValidStudyAcceptHeaderDescriptor.MediaType, - isTransferSyntaxMandatory: true, - transferSyntaxWhenMissing: null, - acceptableTransferSyntaxes: ValidStudyAcceptHeaderDescriptor.AcceptableTransferSyntaxes); - - Assert.True(descriptor.IsTransferSyntaxMandatory); - Assert.Null(descriptor.TransferSyntaxWhenMissing); - - Assert.Null( - descriptor.GetTransferSyntax( - new( - ValidStudyAcceptHeaderDescriptor.MediaType, - ValidStudyAcceptHeaderDescriptor.PayloadType, - transferSyntax: null) - ).Value - ); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Retrieve/AcceptHeaderHandlerTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Retrieve/AcceptHeaderHandlerTests.cs deleted file mode 100644 index fa587d3ba4..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Retrieve/AcceptHeaderHandlerTests.cs +++ /dev/null @@ -1,255 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Linq; -using FellowOakDicom; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Retrieve; -using Microsoft.Health.Dicom.Core.Messages; -using Microsoft.Health.Dicom.Core.Messages.Retrieve; -using Microsoft.Health.Dicom.Core.Web; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Retrieve; - -public class AcceptHeaderHandlerTests -{ - private readonly AcceptHeaderHandler _handler; - - private static readonly AcceptHeaderDescriptor ValidStudyAcceptHeaderDescriptor = AcceptHeaderHandler - .AcceptableDescriptors[ResourceType.Study] - .First(); - - public static IEnumerable UnacceptableHeadersList() - { - yield return new object[] - { - new List - { - new( - "unsupportedMediaType", - ValidStudyAcceptHeaderDescriptor.PayloadType, - ValidStudyAcceptHeaderDescriptor.TransferSyntaxWhenMissing) - }, - ResourceType.Study - }; - yield return new object[] - { - new List - { - new( - ValidStudyAcceptHeaderDescriptor.MediaType, - ValidStudyAcceptHeaderDescriptor.PayloadType, - "unsupportedTransferSyntax") - }, - ResourceType.Study - }; - yield return new object[] - { - new List - { - new( - "unsupportedMediaType", - ValidStudyAcceptHeaderDescriptor.PayloadType, - ValidStudyAcceptHeaderDescriptor.TransferSyntaxWhenMissing), - new( - ValidStudyAcceptHeaderDescriptor.MediaType, - ValidStudyAcceptHeaderDescriptor.PayloadType, - "unsupportedTransferSyntax") - }, - ResourceType.Study - }; - } - - public static IEnumerable AnyMediaTypeHeadersList() - { - yield return new object[] - { - new List - { - new( - "*/*", - ValidStudyAcceptHeaderDescriptor.PayloadType, - ValidStudyAcceptHeaderDescriptor.TransferSyntaxWhenMissing) - }, - ResourceType.Study, - KnownContentTypes.ApplicationDicom, - PayloadTypes.MultipartRelated, - }; - yield return new object[] - { - new List - { - new( - "*/*", - ValidStudyAcceptHeaderDescriptor.PayloadType) - }, - ResourceType.Series, - KnownContentTypes.ApplicationDicom, - PayloadTypes.MultipartRelated, - }; - yield return new object[] - { - new List - { - new( - "*/*", - ValidStudyAcceptHeaderDescriptor.PayloadType) - }, - ResourceType.Frames, - KnownContentTypes.ApplicationOctetStream, - PayloadTypes.MultipartRelated, - }; - } - - public AcceptHeaderHandlerTests() - { - _handler = new AcceptHeaderHandler(NullLogger.Instance); - } - - [Fact] - public void - GivenASingleRequestedAcceptHeader_WhenRequestedMatchesHeadersWeAccept_ThenShouldReturnAcceptedHeaderWithTransferSyntaxAndDescriptorThatMatched() - { - AcceptHeader requestedAcceptHeader = new AcceptHeader( - ValidStudyAcceptHeaderDescriptor.MediaType, - ValidStudyAcceptHeaderDescriptor.PayloadType, - ValidStudyAcceptHeaderDescriptor.TransferSyntaxWhenMissing - ); - - AcceptHeader matchedAcceptHeader = _handler.GetValidAcceptHeader( - ResourceType.Study, - new List() { requestedAcceptHeader } - ); - - Assert.Equivalent(requestedAcceptHeader, matchedAcceptHeader, strict: true); - } - - [Theory] - [MemberData(nameof(UnacceptableHeadersList))] - public void GivenNoMatchedAcceptHeaders_WhenGetTransferSyntax_ThenShouldThrowNotAcceptableException( - List requestedAcceptHeaders, - ResourceType requestedResourceType) - { - Assert.ThrowsAny(() => _handler.GetValidAcceptHeader( - requestedResourceType, - requestedAcceptHeaders - )); - } - - [Fact] - public void - GivenMultipleMatchedAcceptHeadersWithDifferentQuality_WhenHeadersRequestedAreAllSupported_ThenShouldReturnHighestQuality() - { - Assert.True(ValidStudyAcceptHeaderDescriptor.AcceptableTransferSyntaxes.Count > 1); - - AcceptHeader requestedAcceptHeader1 = new AcceptHeader( - ValidStudyAcceptHeaderDescriptor.MediaType, - ValidStudyAcceptHeaderDescriptor.PayloadType, - ValidStudyAcceptHeaderDescriptor.AcceptableTransferSyntaxes.First(), - quality: 0.5 - ); - - AcceptHeader requestedAcceptHeader2 = new AcceptHeader( - ValidStudyAcceptHeaderDescriptor.MediaType, - ValidStudyAcceptHeaderDescriptor.PayloadType, - ValidStudyAcceptHeaderDescriptor.AcceptableTransferSyntaxes.Last(), - quality: 0.9 - ); - - AcceptHeader matchedAcceptHeader = _handler.GetValidAcceptHeader( - ResourceType.Study, - new[] { requestedAcceptHeader1, requestedAcceptHeader2 } - ); - - Assert.Equivalent(requestedAcceptHeader2, matchedAcceptHeader, strict: true); - } - - [Fact] - public void - GivenMultipleMatchedAcceptHeadersWithDifferentQuality_WhenTransferSyntaxRequestedOfHigherQualityNotSupported_ThenShouldReturnNextHighestQuality() - { - // When we multiple headers requested, but the one with highest quality "preference" - // is requested with a TransferSyntax that we do not support, - // we should use the next highest quality requested with a supported TransferSyntax. - - Assert.True(ValidStudyAcceptHeaderDescriptor.AcceptableTransferSyntaxes.Count > 1); - - AcceptHeader requestedAcceptHeader1 = new AcceptHeader( - ValidStudyAcceptHeaderDescriptor.MediaType, - ValidStudyAcceptHeaderDescriptor.PayloadType, - DicomTransferSyntaxUids.Original, - quality: 0.3 - ); - - AcceptHeader requestedAcceptHeader2 = new AcceptHeader( - ValidStudyAcceptHeaderDescriptor.MediaType, - ValidStudyAcceptHeaderDescriptor.PayloadType, - ValidStudyAcceptHeaderDescriptor.AcceptableTransferSyntaxes.Last(), - quality: 0.5 - ); - - AcceptHeader requestedAcceptHeader3 = new AcceptHeader( - ValidStudyAcceptHeaderDescriptor.MediaType, - ValidStudyAcceptHeaderDescriptor.PayloadType, - "unsupportedTransferSyntax", - quality: 0.7 - ); - - AcceptHeader matchedAcceptHeader = _handler.GetValidAcceptHeader( - ResourceType.Study, - new[] { requestedAcceptHeader1, requestedAcceptHeader2, requestedAcceptHeader3 } - ); - - Assert.Equivalent(requestedAcceptHeader2, matchedAcceptHeader, strict: true); - } - - [Fact] - public void GivenMultipleMatchedAcceptHeaderNoQuality_WhenTransferSyntaxRequested_ThenShouldReturnOriginal() - { - AcceptHeader requestedAcceptHeader1 = new AcceptHeader( - payloadType: PayloadTypes.MultipartRelated, - mediaType: KnownContentTypes.ImageJpeg2000, - transferSyntax: DicomTransferSyntax.JPEG2000Lossless.UID.UID); - - AcceptHeader requestedAcceptHeader2 = new AcceptHeader( - payloadType: PayloadTypes.SinglePart, - mediaType: KnownContentTypes.ApplicationOctetStream, - transferSyntax: DicomTransferSyntaxUids.Original); - - - AcceptHeader matchedAcceptHeader = _handler.GetValidAcceptHeader( - ResourceType.Frames, - new[] { requestedAcceptHeader1, requestedAcceptHeader2 } - ); - - Assert.Equivalent(requestedAcceptHeader2, matchedAcceptHeader, strict: true); - } - - [Theory] - [MemberData(nameof(AnyMediaTypeHeadersList))] - public void - GivenASingleRequestedAcceptHeaderWithAnyMediaType_WhenRequestedMatchesHeadersWeAccept_ThenShouldReturnAcceptedHeaderWithTransferSyntaxAndDescriptorThatMatched( - List requestedAcceptHeaders, - ResourceType requestedResourceType, - string mediaType, - PayloadTypes payloadType) - { - AcceptHeader matchedAcceptHeader = _handler.GetValidAcceptHeader( - requestedResourceType, - requestedAcceptHeaders - ); - - var expectedTransferSyntax = string.IsNullOrEmpty(requestedAcceptHeaders.First().TransferSyntax.Value) ? - "*" : - requestedAcceptHeaders.First().TransferSyntax.Value; - - Assert.Equal(mediaType, matchedAcceptHeader.MediaType); - Assert.Equal(payloadType, matchedAcceptHeader.PayloadType); - Assert.Equal(expectedTransferSyntax, matchedAcceptHeader.TransferSyntax); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Retrieve/ETagGeneratorTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Retrieve/ETagGeneratorTests.cs deleted file mode 100644 index 6ccd9f50ae..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Retrieve/ETagGeneratorTests.cs +++ /dev/null @@ -1,106 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Features.Retrieve; -using Microsoft.Health.Dicom.Core.Messages; -using Microsoft.Health.Dicom.Tests.Common; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Retrieve; - -public class ETagGeneratorTests -{ - private readonly IETagGenerator _eTagGenerator; - - private readonly string _studyInstanceUid = TestUidGenerator.Generate(); - private readonly string _seriesInstanceUid = TestUidGenerator.Generate(); - private readonly string _sopInstanceUid = TestUidGenerator.Generate(); - - public ETagGeneratorTests() - { - _eTagGenerator = new ETagGenerator(); - } - - [Fact] - public void GivenETagGenerationRequestForStudy_ExpectedETagIsReturned() - { - List instanceIdentifiers = SetupInstanceIdentifiersList(ResourceType.Study); - string eTag = _eTagGenerator.GetETag(ResourceType.Study, instanceIdentifiers); - string expectedETag = GetExpectedETag(ResourceType.Study, instanceIdentifiers); - - Assert.Equal(expectedETag, eTag); - } - - [Fact] - public void GivenETagGenerationRequestForSeries_ExpectedETagIsReturned() - { - List instanceIdentifiers = SetupInstanceIdentifiersList(ResourceType.Series); - string eTag = _eTagGenerator.GetETag(ResourceType.Series, instanceIdentifiers); - string expectedETag = GetExpectedETag(ResourceType.Series, instanceIdentifiers); - - Assert.Equal(expectedETag, eTag); - } - - [Fact] - public void GivenETagGenerationRequestForInstance_ExpectedETagIsReturned() - { - List instanceIdentifiers = SetupInstanceIdentifiersList(ResourceType.Instance); - string eTag = _eTagGenerator.GetETag(ResourceType.Instance, instanceIdentifiers); - string expectedETag = GetExpectedETag(ResourceType.Instance, instanceIdentifiers); - - Assert.Equal(expectedETag, eTag); - } - - private static string GetExpectedETag(ResourceType resourceType, List instanceIdentifiers) - { - string eTag = string.Empty; - - if (instanceIdentifiers != null && instanceIdentifiers.Count > 0) - { - long maxWatermark = instanceIdentifiers.Max(vii => vii.VersionedInstanceIdentifier.Version); - - switch (resourceType) - { - case ResourceType.Study: - case ResourceType.Series: - int countInstances = instanceIdentifiers.Count; - eTag = $"{maxWatermark}-{countInstances}"; - break; - case ResourceType.Instance: - eTag = maxWatermark.ToString(CultureInfo.InvariantCulture); - break; - default: - break; - } - } - - return eTag; - } - - private List SetupInstanceIdentifiersList(ResourceType resourceType) - { - List dicomInstanceIdentifiersList = new List(); - switch (resourceType) - { - case ResourceType.Study: - dicomInstanceIdentifiersList.Add(new InstanceMetadata(new VersionedInstanceIdentifier(_studyInstanceUid, TestUidGenerator.Generate(), TestUidGenerator.Generate(), version: 0), new InstanceProperties())); - dicomInstanceIdentifiersList.Add(new InstanceMetadata(new VersionedInstanceIdentifier(_studyInstanceUid, TestUidGenerator.Generate(), TestUidGenerator.Generate(), version: 1), new InstanceProperties())); - break; - case ResourceType.Series: - dicomInstanceIdentifiersList.Add(new InstanceMetadata(new VersionedInstanceIdentifier(_studyInstanceUid, _seriesInstanceUid, TestUidGenerator.Generate(), version: 0), new InstanceProperties())); - dicomInstanceIdentifiersList.Add(new InstanceMetadata(new VersionedInstanceIdentifier(_studyInstanceUid, _seriesInstanceUid, TestUidGenerator.Generate(), version: 1), new InstanceProperties())); - break; - case ResourceType.Instance: - dicomInstanceIdentifiersList.Add(new InstanceMetadata(new VersionedInstanceIdentifier(_studyInstanceUid, _seriesInstanceUid, _sopInstanceUid, version: 0), new InstanceProperties())); - break; - } - - return dicomInstanceIdentifiersList; - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Retrieve/FrameHandlerTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Retrieve/FrameHandlerTests.cs deleted file mode 100644 index 04b4fcec21..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Retrieve/FrameHandlerTests.cs +++ /dev/null @@ -1,136 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using EnsureThat; -using FellowOakDicom; -using FellowOakDicom.Imaging; -using FellowOakDicom.IO.Buffer; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Retrieve; -using Microsoft.Health.Dicom.Tests.Common; -using Microsoft.IO; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Retrieve; - -public class FrameHandlerTests -{ - private readonly IFrameHandler _frameHandler; - private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager; - - public FrameHandlerTests() - { - _recyclableMemoryStreamManager = new RecyclableMemoryStreamManager(); - _frameHandler = new FrameHandler(Substitute.For(), _recyclableMemoryStreamManager); - } - - [Theory] - [InlineData(0)] - [InlineData(1, 2)] - [InlineData(0, 1, 2)] - public async Task GivenDicomFileWithFrames_WhenRetrievingFrameWithOriginalTransferSyntax_ThenExpectedFramesAreReturned(params int[] frames) - { - EnsureArg.IsNotNull(frames, nameof(frames)); - (DicomFile file, Stream stream) = await StreamAndStoredFileFromDataset(GenerateDatasetsFromIdentifiers(), 3); - IReadOnlyCollection framesStream = await _frameHandler.GetFramesResourceAsync(stream, frames, true, "*"); - var framesOutput = framesStream.ToArray(); - - for (int i = 0; i < frames.Length; i++) - { - AssertPixelDataEqual(DicomPixelData.Create(file.Dataset).GetFrame(frames[i]), framesOutput[i]); - } - } - - [Theory] - [InlineData(0)] - public async Task GivenDicomFileWithoutFrames_WhenRetrievingFrameWithOriginalTransferSyntax_ThenFrameNotFoundExceptionIsThrown(params int[] frames) - { - (DicomFile file, Stream stream) = await StreamAndStoredFileFromDataset(GenerateDatasetsFromIdentifiers()); - await Assert.ThrowsAsync(() => _frameHandler.GetFramesResourceAsync(stream, frames, true, "*")); - } - - [Theory] - [InlineData(3)] - [InlineData(0, 3)] - [InlineData(0, 1, 2, 3)] - public async Task GivenDicomFileWithFrames_WhenRetrievingNonExistentFrameWithOriginalTransferSyntax_ThenFrameNotFoundExceptionIsThrown(params int[] frames) - { - (DicomFile file, Stream stream) = await StreamAndStoredFileFromDataset(GenerateDatasetsFromIdentifiers(), 3); - await Assert.ThrowsAsync(() => _frameHandler.GetFramesResourceAsync(stream, frames, true, "*")); - } - - [Theory] - [MemberData(nameof(TestDataForInvokingTranscoderOrNotTests))] - public async Task GivenDicomFileWithFrames_WhenRetrievingWithTransferSyntax_ThenTranscoderShouldBeInvokedAsExpected(bool originalTransferSyntaxRequested, string requestedRepresentation, bool shouldBeInvoked) - { - (DicomFile file, Stream stream) = await StreamAndStoredFileFromDataset(GenerateDatasetsFromIdentifiers(), 1); - ITranscoder transcoder = Substitute.For(); - transcoder.TranscodeFrame(Arg.Any(), Arg.Any(), Arg.Any()).Returns(_recyclableMemoryStreamManager.GetStream()); - FrameHandler frameHandler = new FrameHandler(transcoder, _recyclableMemoryStreamManager); - IReadOnlyCollection result = await frameHandler.GetFramesResourceAsync(stream, new int[] { 0 }, originalTransferSyntaxRequested, requestedRepresentation); - - // Call Position of LazyTransformReadOnlyStream so that transcoder.TranscodeFrame is invoked - long pos = result.First().Position; - if (shouldBeInvoked) - { - transcoder.Received().TranscodeFrame(Arg.Any(), Arg.Any(), Arg.Any()); - } - else - { - transcoder.DidNotReceive().TranscodeFrame(Arg.Any(), Arg.Any(), Arg.Any()); - } - } - - public static IEnumerable TestDataForInvokingTranscoderOrNotTests() - { - yield return new object[] { true, DicomTransferSyntaxUids.Original, false }; - yield return new object[] { false, DicomTransferSyntax.ExplicitVRLittleEndian.UID.UID, false }; // Created Dataset is on transferSyntax ExplicitVRLittleEndian - yield return new object[] { false, DicomTransferSyntax.JPEGProcess1.UID.UID, true }; - } - - private static DicomDataset GenerateDatasetsFromIdentifiers() - { - var ds = new DicomDataset(DicomTransferSyntax.ExplicitVRLittleEndian) - { - { DicomTag.StudyInstanceUID, TestUidGenerator.Generate() }, - { DicomTag.SeriesInstanceUID, TestUidGenerator.Generate() }, - { DicomTag.SOPInstanceUID, TestUidGenerator.Generate() }, - { DicomTag.SOPClassUID, TestUidGenerator.Generate() }, - { DicomTag.PatientID, TestUidGenerator.Generate() }, - { DicomTag.BitsAllocated, (ushort)8 }, - { DicomTag.PhotometricInterpretation, PhotometricInterpretation.Monochrome2.Value }, - }; - - return ds; - } - - private async Task<(DicomFile, Stream)> StreamAndStoredFileFromDataset(DicomDataset dataset, int frames = 0) - { - // Create DicomFile associated with input dataset with random pixel data. - var dicomFile = new DicomFile(dataset); - Samples.AppendRandomPixelData(5, 5, frames, dicomFile); - - MemoryStream stream = _recyclableMemoryStreamManager.GetStream(); - await dicomFile.SaveAsync(stream); - stream.Position = 0; - - return (dicomFile, stream); - } - - private static void AssertPixelDataEqual(IByteBuffer expectedPixelData, Stream actualPixelData) - { - Assert.Equal(expectedPixelData.Size, actualPixelData.Length); - Assert.Equal(0, actualPixelData.Position); - for (var i = 0; i < expectedPixelData.Size; i++) - { - Assert.Equal(expectedPixelData.Data[i], actualPixelData.ReadByte()); - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Retrieve/RetrieveHelpers.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Retrieve/RetrieveHelpers.cs deleted file mode 100644 index 8a3b42fc7b..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Retrieve/RetrieveHelpers.cs +++ /dev/null @@ -1,66 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - - - -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; -using FellowOakDicom; -using FellowOakDicom.Imaging; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Tests.Common; -using Microsoft.IO; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Retrieve; -public class RetrieveHelpers -{ - public static DicomDataset GenerateDatasetsFromIdentifiers(InstanceIdentifier instanceIdentifier, string transferSyntaxUid = null) - { - DicomTransferSyntax syntax = DicomTransferSyntax.ExplicitVRLittleEndian; - if (transferSyntaxUid != null) - { - syntax = DicomTransferSyntax.Parse(transferSyntaxUid); - } - - var ds = new DicomDataset(syntax) - { - { DicomTag.StudyInstanceUID, instanceIdentifier.StudyInstanceUid }, - { DicomTag.SeriesInstanceUID, instanceIdentifier.SeriesInstanceUid }, - { DicomTag.SOPInstanceUID, instanceIdentifier.SopInstanceUid }, - { DicomTag.SOPClassUID, TestUidGenerator.Generate() }, - { DicomTag.PatientID, TestUidGenerator.Generate() }, - { DicomTag.BitsAllocated, (ushort)8 }, - { DicomTag.PhotometricInterpretation, PhotometricInterpretation.Monochrome2.Value }, - }; - - return ds; - } - - public static async Task> StreamAndStoredFileFromDataset(DicomDataset dataset, RecyclableMemoryStreamManager recyclableMemoryStreamManager, int rows = 5, int columns = 5, int frames = 0, bool disposeStreams = false) - { - // Create DicomFile associated with input dataset with random pixel data. - var dicomFile = new DicomFile(dataset); - Samples.AppendRandomPixelData(rows, columns, frames, dicomFile); - - if (disposeStreams) - { - using MemoryStream disposableStream = recyclableMemoryStreamManager.GetStream(); - - // Save file to a stream and reset position to 0. - await dicomFile.SaveAsync(disposableStream); - disposableStream.Position = 0; - - return new KeyValuePair(dicomFile, disposableStream); - } - - MemoryStream stream = recyclableMemoryStreamManager.GetStream(); - await dicomFile.SaveAsync(stream); - stream.Position = 0; - - return new KeyValuePair(dicomFile, stream); - } - -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Retrieve/RetrieveMetadataHandlerTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Retrieve/RetrieveMetadataHandlerTests.cs deleted file mode 100644 index ce1a3bc5d0..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Retrieve/RetrieveMetadataHandlerTests.cs +++ /dev/null @@ -1,270 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using FellowOakDicom; -using Microsoft.Health.Core.Features.Security.Authorization; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Retrieve; -using Microsoft.Health.Dicom.Core.Features.Security; -using Microsoft.Health.Dicom.Core.Features.Validation; -using Microsoft.Health.Dicom.Core.Messages.Retrieve; -using Microsoft.Health.Dicom.Tests.Common; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Retrieve; - -public class RetrieveMetadataHandlerTests -{ - private readonly IRetrieveMetadataService _retrieveMetadataService; - private readonly RetrieveMetadataHandler _retrieveMetadataHandler; - - public RetrieveMetadataHandlerTests() - { - _retrieveMetadataService = Substitute.For(); - _retrieveMetadataHandler = new RetrieveMetadataHandler(new DisabledAuthorizationService(), _retrieveMetadataService); - } - - [Theory] - [InlineData("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")] - [InlineData("345%^&")] - [InlineData("()")] - public async Task GivenARequestWithInvalidStudyInstanceIdentifier_WhenHandlerIsExecuted_ThenDicomInvalidIdentifierExceptionIsThrown(string studyInstanceUid) - { - EnsureArg.IsNotNull(studyInstanceUid, nameof(studyInstanceUid)); - string ifNoneMatch = null; - RetrieveMetadataRequest request = new RetrieveMetadataRequest(studyInstanceUid, ifNoneMatch); - var ex = await Assert.ThrowsAsync(() => _retrieveMetadataHandler.Handle(request, CancellationToken.None)); - Assert.Equal(ValidationErrorCode.UidIsInvalid, ex.ErrorCode); - } - - [Theory] - [InlineData("aaaa-bbbb", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")] - [InlineData("aaaa-bbbb", " ")] - [InlineData("aaaa-bbbb", "345%^&")] - [InlineData("aaaa-bbbb", "aaaa-bbbb")] - public async Task GivenARequestWithInvalidStudyIdentifier_WhenRetrievingSeriesMetadata_ThenDicomInvalidIdentifierExceptionIsThrown(string studyInstanceUid, string seriesInstanceUid) - { - EnsureArg.IsNotNull(studyInstanceUid, nameof(studyInstanceUid)); - string ifNoneMatch = null; - RetrieveMetadataRequest request = new RetrieveMetadataRequest(studyInstanceUid, seriesInstanceUid, ifNoneMatch); - var ex = await Assert.ThrowsAsync(() => _retrieveMetadataHandler.Handle(request, CancellationToken.None)); - Assert.Equal(ValidationErrorCode.UidIsInvalid, ex.ErrorCode); - } - - [Theory] - [InlineData("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")] - [InlineData("345%^&")] - [InlineData("aaaa-bbbb")] - [InlineData("()")] - public async Task GivenARequestWithInvalidSeriesIdentifier_WhenRetrievingSeriesMetadata_ThenDicomInvalidIdentifierExceptionIsThrown(string seriesInstanceUid) - { - EnsureArg.IsNotNull(seriesInstanceUid, nameof(seriesInstanceUid)); - string ifNoneMatch = null; - RetrieveMetadataRequest request = new RetrieveMetadataRequest(TestUidGenerator.Generate(), seriesInstanceUid, ifNoneMatch); - var ex = await Assert.ThrowsAsync(() => _retrieveMetadataHandler.Handle(request, CancellationToken.None)); - Assert.Equal(ValidationErrorCode.UidIsInvalid, ex.ErrorCode); - } - - [Theory] - [InlineData("aaaa-bbbb1", "aaaa-bbbb2", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")] - [InlineData("aaaa-bbbb1", "aaaa-bbbb2", "345%^&")] - [InlineData("aaaa-bbbb1", "aaaa-bbbb2", "aaaa-bbbb2")] - [InlineData("aaaa-bbbb1", "aaaa-bbbb2", "aaaa-bbbb1")] - public async Task GivenARequestWithInvalidStudyAndSeriesInstanceIdentifier_WhenRetrievingInstanceMetadata_ThenDicomInvalidIdentifierExceptionIsThrown(string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid) - { - EnsureArg.IsNotNull(studyInstanceUid, nameof(studyInstanceUid)); - string ifNoneMatch = null; - RetrieveMetadataRequest request = new RetrieveMetadataRequest(studyInstanceUid, seriesInstanceUid, sopInstanceUid, ifNoneMatch); - var ex = await Assert.ThrowsAsync(() => _retrieveMetadataHandler.Handle(request, CancellationToken.None)); - Assert.Equal(ValidationErrorCode.UidIsInvalid, ex.ErrorCode); - } - - [Theory] - [InlineData("aaaa-bbbb2", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")] - [InlineData("aaaa-bbbb2", "345%^&")] - [InlineData("aaaa-bbbb2", "aaaa-bbbb2")] - [InlineData("aaaa-bbbb2", " ")] - public async Task GivenARequestWithInvalidSeriesInstanceIdentifier_WhenRetrievingInstanceMetadata_ThenDicomInvalidIdentifierExceptionIsThrown(string seriesInstanceUid, string sopInstanceUid) - { - EnsureArg.IsNotNull(seriesInstanceUid, nameof(seriesInstanceUid)); - string ifNoneMatch = null; - RetrieveMetadataRequest request = new RetrieveMetadataRequest(TestUidGenerator.Generate(), seriesInstanceUid, sopInstanceUid, ifNoneMatch); - var ex = await Assert.ThrowsAsync(() => _retrieveMetadataHandler.Handle(request, CancellationToken.None)); - Assert.Equal(ValidationErrorCode.UidIsInvalid, ex.ErrorCode); - } - - [Theory] - [InlineData("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")] - [InlineData("345%^&")] - [InlineData("aaaa-bbbb")] - [InlineData("()")] - public async Task GivenARequestWithInvalidSopInstanceIdentifier_WhenRetrievingInstanceMetadata_ThenDicomInvalidIdentifierExceptionIsThrown(string sopInstanceUid) - { - EnsureArg.IsNotNull(sopInstanceUid, nameof(sopInstanceUid)); - string ifNoneMatch = null; - RetrieveMetadataRequest request = new RetrieveMetadataRequest(TestUidGenerator.Generate(), TestUidGenerator.Generate(), sopInstanceUid, ifNoneMatch); - var ex = await Assert.ThrowsAsync(() => _retrieveMetadataHandler.Handle(request, CancellationToken.None)); - Assert.Equal(ValidationErrorCode.UidIsInvalid, ex.ErrorCode); - } - - [Theory] - [InlineData("1", "1", "2")] - [InlineData("1", "2", "1")] - [InlineData("1", "2", "2")] - public async Task GivenRepeatedIdentifiers_WhenRetrievingInstanceMetadata_ThenResponseMetadataIsReturnedSuccessfully(string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid) - { - string ifNoneMatch = null; - var request = new RetrieveMetadataRequest( - studyInstanceUid: studyInstanceUid, - seriesInstanceUid: seriesInstanceUid, - sopInstanceUid: sopInstanceUid, - ifNoneMatch: ifNoneMatch); - - RetrieveMetadataResponse response = SetupRetrieveMetadataResponse(); - _retrieveMetadataService.RetrieveSopInstanceMetadataAsync(studyInstanceUid, seriesInstanceUid, sopInstanceUid).Returns(response); - - await ValidateRetrieveMetadataResponse(response, request); - } - - [Fact] - public async Task GivenARequestWithValidInstanceIdentifier_WhenRetrievingStudyInstanceMetadata_ThenResponseMetadataIsReturnedSuccessfully() - { - string studyInstanceUid = TestUidGenerator.Generate(); - string ifNoneMatch = null; - - RetrieveMetadataResponse response = SetupRetrieveMetadataResponse(); - _retrieveMetadataService.RetrieveStudyInstanceMetadataAsync(studyInstanceUid).Returns(response); - - RetrieveMetadataRequest request = new RetrieveMetadataRequest(studyInstanceUid, ifNoneMatch); - await ValidateRetrieveMetadataResponse(response, request); - } - - [Theory] - [InlineData("2.25.282803907956301170169364749856339309473", "1-1")] - public async Task GivenARequestWithValidInstanceIdentifierAndIfNoneMatchHeader_WhenRetrievingStudyInstanceMetadata_ThenNotModifiedResponseIsReturned(string studyInstanceUid, string ifNoneMatch) - { - RetrieveMetadataResponse response = SetupRetrieveMetadataResponseForValidatingCache(true, ifNoneMatch); - _retrieveMetadataService.RetrieveStudyInstanceMetadataAsync(studyInstanceUid, ifNoneMatch).Returns(response); - - RetrieveMetadataRequest request = new RetrieveMetadataRequest(studyInstanceUid, ifNoneMatch); - await ValidateRetrieveMetadataResponse(response, request); - } - - [Theory] - [InlineData("2.25.282803907956301170169364749856339309473", "1-1", "2-2")] - public async Task GivenARequestWithValidInstanceIdentifierAndExpiredIfNoneMatchHeader_WhenRetrievingStudyInstanceMetadata_ThenResponseMetadataIsReturnedSuccessfully(string studyInstanceUid, string ifNoneMatch, string eTag) - { - RetrieveMetadataResponse response = SetupRetrieveMetadataResponseForValidatingCache(false, eTag); - _retrieveMetadataService.RetrieveStudyInstanceMetadataAsync(studyInstanceUid, ifNoneMatch).Returns(response); - - RetrieveMetadataRequest request = new RetrieveMetadataRequest(studyInstanceUid, ifNoneMatch); - await ValidateRetrieveMetadataResponse(response, request); - } - - [Fact] - public async Task GivenARequestWithValidInstanceIdentifier_WhenRetrievingSeriesInstanceMetadata_ThenResponseMetadataIsReturnedSuccessfully() - { - string studyInstanceUid = TestUidGenerator.Generate(); - string seriesInstanceUid = TestUidGenerator.Generate(); - string ifNoneMatch = null; - - RetrieveMetadataResponse response = SetupRetrieveMetadataResponse(); - _retrieveMetadataService.RetrieveSeriesInstanceMetadataAsync(studyInstanceUid, seriesInstanceUid).Returns(response); - - RetrieveMetadataRequest request = new RetrieveMetadataRequest(studyInstanceUid, seriesInstanceUid, ifNoneMatch); - await ValidateRetrieveMetadataResponse(response, request); - } - - [Theory] - [InlineData("2.25.282803907956301170169364749856339309473", "2.25.73315770910160804467620423579356140698", "1-1")] - public async Task GivenARequestWithValidInstanceIdentifierAndIfNoneMatchHeader_WhenRetrievingSeriesInstanceMetadata_ThenNotModifiedResponseIsReturned(string studyInstanceUid, string seriesInstanceUid, string ifNoneMatch) - { - RetrieveMetadataResponse response = SetupRetrieveMetadataResponseForValidatingCache(true, ifNoneMatch); - _retrieveMetadataService.RetrieveSeriesInstanceMetadataAsync(studyInstanceUid, seriesInstanceUid, ifNoneMatch).Returns(response); - - RetrieveMetadataRequest request = new RetrieveMetadataRequest(studyInstanceUid, seriesInstanceUid, ifNoneMatch); - await ValidateRetrieveMetadataResponse(response, request); - } - - [Theory] - [InlineData("2.25.282803907956301170169364749856339309473", "2.25.73315770910160804467620423579356140698", "1-1", "2-2")] - public async Task GivenARequestWithValidInstanceIdentifierAndExpiredIfNoneMatchHeader_WhenRetrievingSeriesInstanceMetadata_ThenResponseMetadataIsReturnedSuccessfully(string studyInstanceUid, string seriesInstanceUid, string ifNoneMatch, string eTag) - { - RetrieveMetadataResponse response = SetupRetrieveMetadataResponseForValidatingCache(false, eTag); - _retrieveMetadataService.RetrieveSeriesInstanceMetadataAsync(studyInstanceUid, seriesInstanceUid, ifNoneMatch).Returns(response); - - RetrieveMetadataRequest request = new RetrieveMetadataRequest(studyInstanceUid, seriesInstanceUid, ifNoneMatch); - await ValidateRetrieveMetadataResponse(response, request); - } - - [Fact] - public async Task GivenARequestWithValidInstanceIdentifier_WhenRetrievingSopInstanceMetadata_ThenResponseMetadataIsReturnedSuccessfully() - { - string studyInstanceUid = TestUidGenerator.Generate(); - string seriesInstanceUid = TestUidGenerator.Generate(); - string sopInstanceUid = TestUidGenerator.Generate(); - string ifNoneMatch = null; - - RetrieveMetadataResponse response = SetupRetrieveMetadataResponse(); - _retrieveMetadataService.RetrieveSopInstanceMetadataAsync(studyInstanceUid, seriesInstanceUid, sopInstanceUid).Returns(response); - - RetrieveMetadataRequest request = new RetrieveMetadataRequest(studyInstanceUid, seriesInstanceUid, sopInstanceUid, ifNoneMatch); - await ValidateRetrieveMetadataResponse(response, request); - } - - [Theory] - [InlineData("2.25.282803907956301170169364749856339309473", "2.25.73315770910160804467620423579356140698", "2.25.224979845195287507011636517849022735847", "1-1")] - public async Task GivenARequestWithValidInstanceIdentifierAndIfNoneMatchHeader_WhenRetrievingSopInstanceMetadata_ThenNotModifiedResponseIsReturned(string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid, string ifNoneMatch) - { - RetrieveMetadataResponse response = SetupRetrieveMetadataResponseForValidatingCache(true, ifNoneMatch); - _retrieveMetadataService.RetrieveSopInstanceMetadataAsync(studyInstanceUid, seriesInstanceUid, sopInstanceUid, ifNoneMatch).Returns(response); - - RetrieveMetadataRequest request = new RetrieveMetadataRequest(studyInstanceUid, seriesInstanceUid, sopInstanceUid, ifNoneMatch); - await ValidateRetrieveMetadataResponse(response, request); - } - - [Theory] - [InlineData("2.25.282803907956301170169364749856339309473", "2.25.73315770910160804467620423579356140698", "2.25.224979845195287507011636517849022735847", "1-1", "2-2")] - public async Task GivenARequestWithValidInstanceIdentifierAndExpiredIfNoneMatchHeader_WhenRetrievingSopInstanceMetadata_ThenResponseMetadataIsReturnedSuccessfully(string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid, string ifNoneMatch, string eTag) - { - RetrieveMetadataResponse response = SetupRetrieveMetadataResponseForValidatingCache(false, eTag); - _retrieveMetadataService.RetrieveSopInstanceMetadataAsync(studyInstanceUid, seriesInstanceUid, sopInstanceUid, ifNoneMatch).Returns(response); - - RetrieveMetadataRequest request = new RetrieveMetadataRequest(studyInstanceUid, seriesInstanceUid, sopInstanceUid, ifNoneMatch); - await ValidateRetrieveMetadataResponse(response, request); - } - - private static RetrieveMetadataResponse SetupRetrieveMetadataResponse() - { - return new RetrieveMetadataResponse(new List { new DicomDataset() }.ToAsyncEnumerable()); - } - - private static RetrieveMetadataResponse SetupRetrieveMetadataResponseForValidatingCache(bool isCacheValid, string eTag) - { - List responseMetadata = new List(); - - if (!isCacheValid) - { - responseMetadata.Add(new DicomDataset()); - } - - return new RetrieveMetadataResponse( - responseMetadata.ToAsyncEnumerable(), - isCacheValid: isCacheValid, - eTag: eTag); - } - - private async Task ValidateRetrieveMetadataResponse(RetrieveMetadataResponse response, RetrieveMetadataRequest request) - { - RetrieveMetadataResponse actualResponse = await _retrieveMetadataHandler.Handle(request, CancellationToken.None); - Assert.Same(response, actualResponse); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Retrieve/RetrieveMetadataServiceTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Retrieve/RetrieveMetadataServiceTests.cs deleted file mode 100644 index 3a4ae3046a..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Retrieve/RetrieveMetadataServiceTests.cs +++ /dev/null @@ -1,257 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using FellowOakDicom; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Context; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Core.Features.Retrieve; -using Microsoft.Health.Dicom.Core.Features.Telemetry; -using Microsoft.Health.Dicom.Core.Messages; -using Microsoft.Health.Dicom.Core.Messages.Retrieve; -using Microsoft.Health.Dicom.Tests.Common; -using NSubstitute; -using NSubstitute.ExceptionExtensions; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Retrieve; - -public class RetrieveMetadataServiceTests -{ - private readonly IInstanceStore _instanceStore; - private readonly IMetadataStore _metadataStore; - private readonly IETagGenerator _eTagGenerator; - private readonly RetrieveMetadataService _retrieveMetadataService; - private readonly IDicomRequestContextAccessor _dicomRequestContextAccessor; - private readonly RetrieveMeter _retrieveMeter; - - private readonly string _studyInstanceUid = TestUidGenerator.Generate(); - private readonly string _seriesInstanceUid = TestUidGenerator.Generate(); - private readonly string _sopInstanceUid = TestUidGenerator.Generate(); - private static readonly CancellationToken DefaultCancellationToken = new CancellationTokenSource().Token; - - public RetrieveMetadataServiceTests() - { - _instanceStore = Substitute.For(); - _metadataStore = Substitute.For(); - _eTagGenerator = Substitute.For(); - _dicomRequestContextAccessor = Substitute.For(); - _retrieveMeter = new RetrieveMeter(); - - _dicomRequestContextAccessor.RequestContext.DataPartition = Partition.Default; - _retrieveMetadataService = new RetrieveMetadataService( - _instanceStore, - _metadataStore, - _eTagGenerator, - _dicomRequestContextAccessor, - _retrieveMeter, - Options.Create(new RetrieveConfiguration())); - } - - [Fact] - public async Task GivenRetrieveStudyMetadataRequest_WhenStudyInstanceUidDoesNotExist_ThenDicomInstanceNotFoundExceptionIsThrownAsync() - { - string ifNoneMatch = null; - InstanceNotFoundException exception = await Assert.ThrowsAsync(() => _retrieveMetadataService.RetrieveStudyInstanceMetadataAsync(TestUidGenerator.Generate(), ifNoneMatch, cancellationToken: DefaultCancellationToken)); - Assert.Equal("The specified study cannot be found.", exception.Message); - } - - [Fact] - public async Task GivenRetrieveSeriesMetadataRequest_WhenStudyAndSeriesInstanceUidDoesNotExist_ThenDicomInstanceNotFoundExceptionIsThrownAsync() - { - string ifNoneMatch = null; - InstanceNotFoundException exception = await Assert.ThrowsAsync(() => _retrieveMetadataService.RetrieveSeriesInstanceMetadataAsync(TestUidGenerator.Generate(), TestUidGenerator.Generate(), ifNoneMatch, cancellationToken: DefaultCancellationToken)); - Assert.Equal("The specified series cannot be found.", exception.Message); - } - - [Fact] - public async Task GivenRetrieveSeriesMetadataRequest_WhenStudyInstanceUidDoesNotExist_ThenDicomInstanceNotFoundExceptionIsThrownAsync() - { - SetupInstanceIdentifiersList(ResourceType.Series, _dicomRequestContextAccessor.RequestContext.DataPartition); - - string ifNoneMatch = null; - InstanceNotFoundException exception = await Assert.ThrowsAsync(() => _retrieveMetadataService.RetrieveSeriesInstanceMetadataAsync(TestUidGenerator.Generate(), _seriesInstanceUid, ifNoneMatch, cancellationToken: DefaultCancellationToken)); - Assert.Equal("The specified series cannot be found.", exception.Message); - } - - [Fact] - public async Task GivenRetrieveSeriesMetadataRequest_WhenSeriesInstanceUidDoesNotExist_ThenDicomInstanceNotFoundExceptionIsThrownAsync() - { - SetupInstanceIdentifiersList(ResourceType.Series, _dicomRequestContextAccessor.RequestContext.DataPartition); - - string ifNoneMatch = null; - InstanceNotFoundException exception = await Assert.ThrowsAsync(() => _retrieveMetadataService.RetrieveSeriesInstanceMetadataAsync(_studyInstanceUid, TestUidGenerator.Generate(), ifNoneMatch, cancellationToken: DefaultCancellationToken)); - Assert.Equal("The specified series cannot be found.", exception.Message); - } - - [Fact] - public async Task GivenRetrieveSopInstanceMetadataRequest_WhenStudySeriesAndSopInstanceUidDoesNotExist_ThenDicomInstanceNotFoundExceptionIsThrownAsync() - { - string ifNoneMatch = null; - InstanceNotFoundException exception = await Assert.ThrowsAsync(() => _retrieveMetadataService.RetrieveSopInstanceMetadataAsync(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), ifNoneMatch, cancellationToken: DefaultCancellationToken)); - Assert.Equal("The specified instance cannot be found.", exception.Message); - } - - [Fact] - public async Task GivenRetrieveSopInstanceMetadataRequest_WhenStudyAndSeriesDoesNotExist_ThenDicomInstanceNotFoundExceptionIsThrownAsync() - { - SetupInstanceIdentifiersList(ResourceType.Instance, _dicomRequestContextAccessor.RequestContext.DataPartition); - - string ifNoneMatch = null; - InstanceNotFoundException exception = await Assert.ThrowsAsync(() => _retrieveMetadataService.RetrieveSopInstanceMetadataAsync(TestUidGenerator.Generate(), TestUidGenerator.Generate(), _sopInstanceUid, ifNoneMatch, cancellationToken: DefaultCancellationToken)); - Assert.Equal("The specified instance cannot be found.", exception.Message); - } - - [Fact] - public async Task GivenRetrieveSopInstanceMetadataRequest_WhenSeriesInstanceUidDoesNotExist_ThenDicomInstanceNotFoundExceptionIsThrownAsync() - { - SetupInstanceIdentifiersList(ResourceType.Instance, _dicomRequestContextAccessor.RequestContext.DataPartition); - - string ifNoneMatch = null; - InstanceNotFoundException exception = await Assert.ThrowsAsync(() => _retrieveMetadataService.RetrieveSopInstanceMetadataAsync(_studyInstanceUid, TestUidGenerator.Generate(), _sopInstanceUid, ifNoneMatch, cancellationToken: DefaultCancellationToken)); - Assert.Equal("The specified instance cannot be found.", exception.Message); - } - - [Fact] - public async Task GivenRetrieveInstanceMetadataRequestForStudy_WhenFailsToRetrieveSome_ThenDicomInstanceNotFoundExceptionIsThrownAsync() - { - List versionedInstanceIdentifiers = SetupInstanceIdentifiersList(ResourceType.Study, _dicomRequestContextAccessor.RequestContext.DataPartition); - - _metadataStore.GetInstanceMetadataAsync(versionedInstanceIdentifiers.Last().VersionedInstanceIdentifier.Version, Arg.Any()).Throws(new InstanceNotFoundException()); - _metadataStore.GetInstanceMetadataAsync(versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Version, Arg.Any()).Returns(new DicomDataset()); - - string ifNoneMatch = null; - RetrieveMetadataResponse response = await _retrieveMetadataService.RetrieveStudyInstanceMetadataAsync(_studyInstanceUid, ifNoneMatch, cancellationToken: DefaultCancellationToken); - InstanceNotFoundException exception = await Assert.ThrowsAsync(() => response.ResponseMetadata.ToListAsync().AsTask()); - Assert.Equal("The specified instance cannot be found.", exception.Message); - } - - [Fact] - public async Task GivenRetrieveInstanceMetadataRequestForStudy_WhenIsSuccessful_ThenSuccessStatusCodeIsReturnedAsync() - { - List versionedInstanceIdentifiers = SetupInstanceIdentifiersList(ResourceType.Study, _dicomRequestContextAccessor.RequestContext.DataPartition); - - _metadataStore.GetInstanceMetadataAsync(versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Version, DefaultCancellationToken).Returns(new DicomDataset()); - _metadataStore.GetInstanceMetadataAsync(versionedInstanceIdentifiers.Last().VersionedInstanceIdentifier.Version, DefaultCancellationToken).Returns(new DicomDataset()); - - string ifNoneMatch = null; - RetrieveMetadataResponse response = await _retrieveMetadataService.RetrieveStudyInstanceMetadataAsync(_studyInstanceUid, ifNoneMatch, cancellationToken: DefaultCancellationToken); - - Assert.Equal(await response.ResponseMetadata.CountAsync(), versionedInstanceIdentifiers.Count); - Assert.Equal(await response.ResponseMetadata.CountAsync(), _dicomRequestContextAccessor.RequestContext.PartCount); - } - - [Fact] - public async Task GivenRetrieveInstanceMetadataRequestWithOriginalVersionForStudy_WhenIsSuccessful_ThenSuccessStatusCodeIsReturnedAsync() - { - IReadOnlyList versionedInstanceIdentifiers = SetupInstanceIdentifiersList( - ResourceType.Study, - instanceProperty: new InstanceProperties() { OriginalVersion = 5 }, - isInitialVersion: true); - - _metadataStore.GetInstanceMetadataAsync(5, DefaultCancellationToken).Returns(new DicomDataset()); - - string ifNoneMatch = null; - RetrieveMetadataResponse response = await _retrieveMetadataService.RetrieveStudyInstanceMetadataAsync(_studyInstanceUid, ifNoneMatch, isOriginalVersionRequested: true, DefaultCancellationToken); - - await response.ResponseMetadata.CountAsync(); - - await _metadataStore - .Received(2) - .GetInstanceMetadataAsync(Arg.Is(x => x == 5), Arg.Any()); - } - - [Fact] - public async Task GivenRetrieveInstanceMetadataRequestForSeries_WhenFailsToRetrieveSome_ThenDicomInstanceNotFoundExceptionIsThrownAsync() - { - List versionedInstanceIdentifiers = SetupInstanceIdentifiersList(ResourceType.Series, _dicomRequestContextAccessor.RequestContext.DataPartition); - - _metadataStore.GetInstanceMetadataAsync(versionedInstanceIdentifiers.Last().VersionedInstanceIdentifier.Version, Arg.Any()).Throws(new InstanceNotFoundException()); - _metadataStore.GetInstanceMetadataAsync(versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Version, Arg.Any()).Returns(new DicomDataset()); - - string ifNoneMatch = null; - RetrieveMetadataResponse response = await _retrieveMetadataService.RetrieveSeriesInstanceMetadataAsync(_studyInstanceUid, _seriesInstanceUid, ifNoneMatch, cancellationToken: DefaultCancellationToken); - InstanceNotFoundException exception = await Assert.ThrowsAsync(() => response.ResponseMetadata.ToListAsync().AsTask()); - Assert.Equal("The specified instance cannot be found.", exception.Message); - } - - [Fact] - public async Task GivenRetrieveInstanceMetadataRequestForSeries_WhenIsSuccessful_ThenSuccessStatusCodeIsReturnedAsync() - { - List versionedInstanceIdentifiers = SetupInstanceIdentifiersList(ResourceType.Series, _dicomRequestContextAccessor.RequestContext.DataPartition); - - _metadataStore.GetInstanceMetadataAsync(versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Version, DefaultCancellationToken).Returns(new DicomDataset()); - _metadataStore.GetInstanceMetadataAsync(versionedInstanceIdentifiers.Last().VersionedInstanceIdentifier.Version, DefaultCancellationToken).Returns(new DicomDataset()); - - string ifNoneMatch = null; - RetrieveMetadataResponse response = await _retrieveMetadataService.RetrieveSeriesInstanceMetadataAsync(_studyInstanceUid, _seriesInstanceUid, ifNoneMatch, cancellationToken: DefaultCancellationToken); - - Assert.Equal(await response.ResponseMetadata.CountAsync(), versionedInstanceIdentifiers.Count); - Assert.Equal(await response.ResponseMetadata.CountAsync(), _dicomRequestContextAccessor.RequestContext.PartCount); - } - - [Fact] - public async Task GivenRetrieveInstanceMetadataRequestForInstance_WhenFailsToRetrieve_ThenDicomInstanceNotFoundExceptionIsThrownAsync() - { - InstanceMetadata sopInstanceIdentifier = SetupInstanceIdentifiersList(ResourceType.Instance, _dicomRequestContextAccessor.RequestContext.DataPartition).First(); - - _metadataStore.GetInstanceMetadataAsync(sopInstanceIdentifier.VersionedInstanceIdentifier.Version, Arg.Any()).Throws(new InstanceNotFoundException()); - - string ifNoneMatch = null; - RetrieveMetadataResponse response = await _retrieveMetadataService.RetrieveSopInstanceMetadataAsync(_studyInstanceUid, _seriesInstanceUid, _sopInstanceUid, ifNoneMatch, cancellationToken: DefaultCancellationToken); - InstanceNotFoundException exception = await Assert.ThrowsAsync(() => response.ResponseMetadata.ToListAsync().AsTask()); - Assert.Equal("The specified instance cannot be found.", exception.Message); - } - - [Fact] - public async Task GivenRetrieveInstanceMetadataRequestForInstance_WhenIsSuccessful_ThenSuccessStatusCodeIsReturnedAsync() - { - InstanceMetadata sopInstanceIdentifier = SetupInstanceIdentifiersList(ResourceType.Instance, _dicomRequestContextAccessor.RequestContext.DataPartition).First(); - - _metadataStore.GetInstanceMetadataAsync(sopInstanceIdentifier.VersionedInstanceIdentifier.Version, DefaultCancellationToken).Returns(new DicomDataset()); - - string ifNoneMatch = null; - RetrieveMetadataResponse response = await _retrieveMetadataService.RetrieveSopInstanceMetadataAsync(_studyInstanceUid, _seriesInstanceUid, _sopInstanceUid, ifNoneMatch, cancellationToken: DefaultCancellationToken); - - Assert.Equal(1, await response.ResponseMetadata.CountAsync()); - Assert.Equal(1, _dicomRequestContextAccessor.RequestContext.PartCount); - } - - private List SetupInstanceIdentifiersList(ResourceType resourceType, Partition partition = null, InstanceProperties instanceProperty = null, bool isInitialVersion = false) - { - var dicomInstanceIdentifiersList = new List(); - - instanceProperty = instanceProperty ?? new InstanceProperties(); - partition = partition ?? Partition.Default; - - switch (resourceType) - { - case ResourceType.Study: - dicomInstanceIdentifiersList.Add(new InstanceMetadata(new VersionedInstanceIdentifier(_studyInstanceUid, TestUidGenerator.Generate(), TestUidGenerator.Generate(), version: 0), instanceProperty)); - dicomInstanceIdentifiersList.Add(new InstanceMetadata(new VersionedInstanceIdentifier(_studyInstanceUid, TestUidGenerator.Generate(), TestUidGenerator.Generate(), version: 1), instanceProperty)); - _instanceStore.GetInstanceIdentifierWithPropertiesAsync(Arg.Is(x => x.Key == partition.Key), _studyInstanceUid, isInitialVersion: isInitialVersion, cancellationToken: DefaultCancellationToken).Returns(dicomInstanceIdentifiersList); - break; - case ResourceType.Series: - dicomInstanceIdentifiersList.Add(new InstanceMetadata(new VersionedInstanceIdentifier(_studyInstanceUid, _seriesInstanceUid, TestUidGenerator.Generate(), version: 0), instanceProperty)); - dicomInstanceIdentifiersList.Add(new InstanceMetadata(new VersionedInstanceIdentifier(_studyInstanceUid, _seriesInstanceUid, TestUidGenerator.Generate(), version: 1), instanceProperty)); - _instanceStore.GetInstanceIdentifierWithPropertiesAsync(Arg.Is(x => x.Key == partition.Key), _studyInstanceUid, _seriesInstanceUid, isInitialVersion: isInitialVersion, cancellationToken: DefaultCancellationToken).Returns(dicomInstanceIdentifiersList); - break; - case ResourceType.Instance: - dicomInstanceIdentifiersList.Add(new InstanceMetadata(new VersionedInstanceIdentifier(_studyInstanceUid, _seriesInstanceUid, _sopInstanceUid, version: 0), instanceProperty)); - _instanceStore.GetInstanceIdentifierWithPropertiesAsync(Arg.Is(x => x.Key == partition.Key), _studyInstanceUid, _seriesInstanceUid, _sopInstanceUid, isInitialVersion: isInitialVersion, DefaultCancellationToken).Returns(dicomInstanceIdentifiersList); - break; - } - - return dicomInstanceIdentifiersList; - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Retrieve/RetrieveRenderedHandlerTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Retrieve/RetrieveRenderedHandlerTests.cs deleted file mode 100644 index e07b832ca1..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Retrieve/RetrieveRenderedHandlerTests.cs +++ /dev/null @@ -1,107 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - - - -using EnsureThat; -using System.Threading.Tasks; -using Microsoft.Health.Core.Features.Security.Authorization; -using Microsoft.Health.Dicom.Core.Features.Retrieve; -using Microsoft.Health.Dicom.Core.Features.Security; -using NSubstitute; -using Xunit; -using Microsoft.Health.Dicom.Core.Messages.Retrieve; -using Microsoft.Health.Dicom.Tests.Common; -using Microsoft.Health.Dicom.Core.Messages; -using Microsoft.Health.Dicom.Core.Exceptions; -using System.Threading; -using Microsoft.Health.Dicom.Core.Features.Validation; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Retrieve; -public class RetrieveRenderedHandlerTests -{ - - private readonly IRetrieveRenderedService _retrieveRenderedService; - private readonly RetrieveRenderedHandler _retrieveRenderedHandler; - - public RetrieveRenderedHandlerTests() - { - _retrieveRenderedService = Substitute.For(); - _retrieveRenderedHandler = new RetrieveRenderedHandler(new DisabledAuthorizationService(), _retrieveRenderedService); - } - - [Theory] - [InlineData("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")] - [InlineData("345%^&")] - [InlineData("()")] - public async Task GivenARequestWithInvalidStudyInstanceIdentifier_WhenHandlerIsExecuted_ThenDicomInvalidIdentifierExceptionIsThrown(string studyInstanceUid) - { - EnsureArg.IsNotNull(studyInstanceUid, nameof(studyInstanceUid)); - string seriesInstanceUid = TestUidGenerator.Generate(); - string sopInstanceUid = TestUidGenerator.Generate(); - - RetrieveRenderedRequest request = new RetrieveRenderedRequest(studyInstanceUid, seriesInstanceUid, sopInstanceUid, ResourceType.Instance, 0, 75, new[] { AcceptHeaderHelpers.CreateRenderAcceptHeader() }); - var ex = await Assert.ThrowsAsync(() => _retrieveRenderedHandler.Handle(request, CancellationToken.None)); - Assert.Equal(ValidationErrorCode.UidIsInvalid, ex.ErrorCode); - } - - - [Theory] - [InlineData("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")] - [InlineData("345%^&")] - [InlineData("()")] - public async Task GivenARequestWithInvalidSeriesInstanceIdentifier_WhenHandlerIsExecuted_ThenDicomInvalidIdentifierExceptionIsThrown(string seriesInstanceUid) - { - EnsureArg.IsNotNull(seriesInstanceUid, nameof(seriesInstanceUid)); - string studyInstanceUid = TestUidGenerator.Generate(); - string sopInstanceUid = TestUidGenerator.Generate(); - - RetrieveRenderedRequest request = new RetrieveRenderedRequest(studyInstanceUid, seriesInstanceUid, sopInstanceUid, ResourceType.Instance, 0, 75, new[] { AcceptHeaderHelpers.CreateRenderAcceptHeader() }); - var ex = await Assert.ThrowsAsync(() => _retrieveRenderedHandler.Handle(request, CancellationToken.None)); - Assert.Equal(ValidationErrorCode.UidIsInvalid, ex.ErrorCode); - } - - [Theory] - [InlineData("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")] - [InlineData("345%^&")] - [InlineData("()")] - public async Task GivenARequestWithInvalidInstanceInstanceIdentifier_WhenHandlerIsExecuted_ThenDicomInvalidIdentifierExceptionIsThrown(string sopInstanceUid) - { - EnsureArg.IsNotNull(sopInstanceUid, nameof(sopInstanceUid)); - string studyInstanceUid = TestUidGenerator.Generate(); - string seriesInstanceUid = TestUidGenerator.Generate(); - - RetrieveRenderedRequest request = new RetrieveRenderedRequest(studyInstanceUid, seriesInstanceUid, sopInstanceUid, ResourceType.Instance, 0, 75, new[] { AcceptHeaderHelpers.CreateRenderAcceptHeader() }); - var ex = await Assert.ThrowsAsync(() => _retrieveRenderedHandler.Handle(request, CancellationToken.None)); - Assert.Equal(ValidationErrorCode.UidIsInvalid, ex.ErrorCode); - } - - [Theory] - [InlineData(-10)] - [InlineData(-3)] - public async Task GivenARequestWithInvalidFramNumber_WhenHandlerIsExecuted_ThenBadRequestExceptionIsThrown(int frame) - { - string error = "The specified frames value is not valid. At least one frame must be present, and all requested frames must have value greater than 0."; - string studyInstanceUid = TestUidGenerator.Generate(); - string seriesInstanceUid = TestUidGenerator.Generate(); - string sopInstanceUid = TestUidGenerator.Generate(); - - RetrieveRenderedRequest request = new RetrieveRenderedRequest(studyInstanceUid, seriesInstanceUid, sopInstanceUid, ResourceType.Frames, frame, 75, new[] { AcceptHeaderHelpers.CreateRenderAcceptHeader() }); - var ex = await Assert.ThrowsAsync(() => _retrieveRenderedHandler.Handle(request, CancellationToken.None)); - Assert.Equal(error, ex.Message); - } - - [Fact] - public async Task GivenARequestWithValidInstanceInstanceIdentifier_WhenHandlerIsExecuted_ThenNoExceptionThrown() - { - string studyInstanceUid = TestUidGenerator.Generate(); - string seriesInstanceUid = TestUidGenerator.Generate(); - string sopInstanceUid = TestUidGenerator.Generate(); - - RetrieveRenderedRequest request = new RetrieveRenderedRequest(studyInstanceUid, seriesInstanceUid, sopInstanceUid, ResourceType.Frames, 5, 75, new[] { AcceptHeaderHelpers.CreateRenderAcceptHeader() }); - await _retrieveRenderedHandler.Handle(request, CancellationToken.None); - } - -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Retrieve/RetrieveRenderedServiceTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Retrieve/RetrieveRenderedServiceTests.cs deleted file mode 100644 index 76e033bc00..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Retrieve/RetrieveRenderedServiceTests.cs +++ /dev/null @@ -1,418 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using FellowOakDicom.Imaging; -using FellowOakDicom; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Context; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Core.Features.Retrieve; -using Microsoft.Health.Dicom.Core.Features.Telemetry; -using Microsoft.Health.Dicom.Core.Messages; -using Microsoft.Health.Dicom.Core.Messages.Retrieve; -using Microsoft.Health.Dicom.Tests.Common; -using Microsoft.IO; -using NSubstitute; -using Xunit; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Formats.Jpeg; -using Microsoft.Health.Dicom.Core.Web; -using Xunit.Abstractions; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Retrieve; -public class RetrieveRenderedServiceTests -{ - private readonly RetrieveRenderedService _retrieveRenderedService; - private readonly IInstanceStore _instanceStore; - private readonly IFileStore _fileStore; - private readonly IDicomRequestContextAccessor _dicomRequestContextAccessor; - private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager; - private readonly ILogger _logger; - private readonly string _studyInstanceUid = TestUidGenerator.Generate(); - private readonly string _firstSeriesInstanceUid = TestUidGenerator.Generate(); - private readonly string _sopInstanceUid = TestUidGenerator.Generate(); - private static readonly CancellationToken DefaultCancellationToken = new CancellationTokenSource().Token; - private static readonly FileProperties DefaultFileProperties = new FileProperties() { Path = "default/path/0.dcm", ETag = "123", ContentLength = 123 }; - private readonly RetrieveMeter _retrieveMeter; - - - public RetrieveRenderedServiceTests(ITestOutputHelper output) - { - _instanceStore = Substitute.For(); - _fileStore = Substitute.For(); - _dicomRequestContextAccessor = Substitute.For(); - _dicomRequestContextAccessor.RequestContext.DataPartition = Partition.Default; - var retrieveConfigurationSnapshot = Substitute.For>(); - retrieveConfigurationSnapshot.Value.Returns(new RetrieveConfiguration()); - _retrieveMeter = new RetrieveMeter(); - - _logger = output.ToLogger(); - - _recyclableMemoryStreamManager = new RecyclableMemoryStreamManager(); - _retrieveRenderedService = new RetrieveRenderedService( - _instanceStore, - _fileStore, - _dicomRequestContextAccessor, - retrieveConfigurationSnapshot, - _recyclableMemoryStreamManager, - _retrieveMeter, - _logger - ); - - new DicomSetupBuilder() - .RegisterServices(s => s.AddImageManager()) - .Build(); - } - - [Fact] - public async Task GivenARequestWithMultipleAcceptHeaders_WhenServiceIsExecuted_ThenNotAcceptableExceptionExceptionIsThrown() - { - const string expectedErrorMessage = "The request contains multiple accept headers, which is not supported."; - - string studyInstanceUid = TestUidGenerator.Generate(); - string seriesInstanceUid = TestUidGenerator.Generate(); - string sopInstanceUid = TestUidGenerator.Generate(); - - RetrieveRenderedRequest request = new RetrieveRenderedRequest(studyInstanceUid, seriesInstanceUid, sopInstanceUid, ResourceType.Frames, 5, 75, new[] { AcceptHeaderHelpers.CreateRenderAcceptHeader(), AcceptHeaderHelpers.CreateRenderAcceptHeader() }); - var ex = await Assert.ThrowsAsync(() => _retrieveRenderedService.RetrieveRenderedImageAsync(request, CancellationToken.None)); - Assert.Equal(expectedErrorMessage, ex.Message); - } - - [Fact] - public async Task GivenARequestWithInvalidAcceptHeader_WhenServiceIsExecuted_ThenNotAcceptableExceptionExceptionIsThrown() - { - const string expectedErrorMessage = "The request headers are not acceptable"; - - string studyInstanceUid = TestUidGenerator.Generate(); - string seriesInstanceUid = TestUidGenerator.Generate(); - string sopInstanceUid = TestUidGenerator.Generate(); - - RetrieveRenderedRequest request = new RetrieveRenderedRequest(studyInstanceUid, seriesInstanceUid, sopInstanceUid, ResourceType.Frames, 5, 75, new[] { AcceptHeaderHelpers.CreateAcceptHeaderForGetStudy() }); - var ex = await Assert.ThrowsAsync(() => _retrieveRenderedService.RetrieveRenderedImageAsync(request, CancellationToken.None)); - Assert.Equal(expectedErrorMessage, ex.Message); - } - - [Fact] - public async Task GivenNoStoredInstances_RenderForInstance_ThenNotFoundIsThrown() - { - _instanceStore.GetInstanceIdentifiersInStudyAsync(Partition.Default, _studyInstanceUid).Returns(new List()); - - await Assert.ThrowsAsync(() => _retrieveRenderedService.RetrieveRenderedImageAsync( - new RetrieveRenderedRequest(_studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, ResourceType.Instance, 1, 75, new[] { AcceptHeaderHelpers.CreateRenderAcceptHeader() }), - DefaultCancellationToken)); - } - - [Theory] - [InlineData(0)] - [InlineData(-10)] - [InlineData(101)] - public async Task GivenInvalidQuality_RenderForInstance_ThenBadRequestThrown(int quality) - { - const string expectedErrorMessage = "Image quality must be between 1 and 100 inclusive"; - - string studyInstanceUid = TestUidGenerator.Generate(); - string seriesInstanceUid = TestUidGenerator.Generate(); - string sopInstanceUid = TestUidGenerator.Generate(); - - RetrieveRenderedRequest request = new RetrieveRenderedRequest(studyInstanceUid, seriesInstanceUid, sopInstanceUid, ResourceType.Frames, 5, quality, new[] { AcceptHeaderHelpers.CreateRenderAcceptHeader() }); - var ex = await Assert.ThrowsAsync(() => _retrieveRenderedService.RetrieveRenderedImageAsync(request, CancellationToken.None)); - Assert.Equal(expectedErrorMessage, ex.Message); - } - - [Fact] - public async Task GivenFileSizeTooLarge_RenderForInstance_ThenNotFoundIsThrown() - { - List versionedInstanceIdentifiers = SetupInstanceIdentifiersList(partition: _dicomRequestContextAccessor.RequestContext.DataPartition); - long aboveMaxFileSize = new RetrieveConfiguration().MaxDicomFileSize + 1; - _fileStore.GetFilePropertiesAsync(versionedInstanceIdentifiers[0].VersionedInstanceIdentifier.Version, versionedInstanceIdentifiers[0].VersionedInstanceIdentifier.Partition, DefaultFileProperties, DefaultCancellationToken).Returns(new FileProperties { ContentLength = aboveMaxFileSize }); - - await Assert.ThrowsAsync(() => - _retrieveRenderedService.RetrieveRenderedImageAsync( - new RetrieveRenderedRequest(_studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, ResourceType.Frames, 1, 75, new[] { AcceptHeaderHelpers.CreateRenderAcceptHeader() }), - DefaultCancellationToken)); - - } - - [Fact] - public async Task GivenStoredInstancesWithFrames_WhenRenderRequestForNonExistingFrame_ThenNotFoundIsThrown() - { - List versionedInstanceIdentifiers = SetupInstanceIdentifiersList(partition: _dicomRequestContextAccessor.RequestContext.DataPartition); - - // For the instance, set up the fileStore to return a stream containing the file associated with the identifier with 3 frames. - Stream streamOfStoredFiles = (await RetrieveHelpers.StreamAndStoredFileFromDataset(RetrieveHelpers.GenerateDatasetsFromIdentifiers(versionedInstanceIdentifiers[0].VersionedInstanceIdentifier), _recyclableMemoryStreamManager, frames: 3)).Value; - _fileStore.GetFileAsync(versionedInstanceIdentifiers[0].VersionedInstanceIdentifier.Version, versionedInstanceIdentifiers[0].VersionedInstanceIdentifier.Partition, DefaultFileProperties, DefaultCancellationToken).Returns(streamOfStoredFiles); - _fileStore.GetFilePropertiesAsync(versionedInstanceIdentifiers[0].VersionedInstanceIdentifier.Version, versionedInstanceIdentifiers[0].VersionedInstanceIdentifier.Partition, DefaultFileProperties, DefaultCancellationToken).Returns(new FileProperties { ContentLength = streamOfStoredFiles.Length }); - - var retrieveRenderRequest = new RetrieveRenderedRequest(_studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, ResourceType.Frames, 5, 75, new[] { AcceptHeaderHelpers.CreateRenderAcceptHeader() }); - - await Assert.ThrowsAsync(() => _retrieveRenderedService.RetrieveRenderedImageAsync( - retrieveRenderRequest, - DefaultCancellationToken)); - - // Dispose the stream. - streamOfStoredFiles.Dispose(); - } - - [Fact] - public async Task GivenStoredInstancesWithFramesJpeg_WhenRetrieveRenderedForFrames_ThenEachFrameRenderedSuccesfully() - { - List versionedInstanceIdentifiers = SetupInstanceIdentifiersList(); - - KeyValuePair streamAndStoredFile = await RetrieveHelpers.StreamAndStoredFileFromDataset(RetrieveHelpers.GenerateDatasetsFromIdentifiers(versionedInstanceIdentifiers[0].VersionedInstanceIdentifier), _recyclableMemoryStreamManager, frames: 3); - _fileStore.GetFileAsync(versionedInstanceIdentifiers[0].VersionedInstanceIdentifier.Version, versionedInstanceIdentifiers[0].VersionedInstanceIdentifier.Partition, DefaultFileProperties, DefaultCancellationToken).Returns(streamAndStoredFile.Value); - _fileStore.GetFilePropertiesAsync(versionedInstanceIdentifiers[0].VersionedInstanceIdentifier.Version, versionedInstanceIdentifiers[0].VersionedInstanceIdentifier.Partition, DefaultFileProperties, DefaultCancellationToken).Returns(new FileProperties { ContentLength = streamAndStoredFile.Value.Length }); - - MemoryStream copyStream = _recyclableMemoryStreamManager.GetStream(); - await streamAndStoredFile.Value.CopyToAsync(copyStream); - copyStream.Position = 0; - streamAndStoredFile.Value.Position = 0; - - MemoryStream streamAndStoredFileForFrame2 = _recyclableMemoryStreamManager.GetStream(); - await streamAndStoredFile.Value.CopyToAsync(streamAndStoredFileForFrame2); - streamAndStoredFileForFrame2.Position = 0; - streamAndStoredFile.Value.Position = 0; - - var retrieveRenderedRequest = new RetrieveRenderedRequest(_studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, ResourceType.Frames, 1, 75, new[] { AcceptHeaderHelpers.CreateRenderAcceptHeader() }); - - RetrieveRenderedResponse response = await _retrieveRenderedService.RetrieveRenderedImageAsync( - retrieveRenderedRequest, - DefaultCancellationToken); - - DicomFile dicomFile = await DicomFile.OpenAsync(copyStream, FileReadOption.ReadLargeOnDemand); - DicomImage dicomImage = new DicomImage(dicomFile.Dataset); - using var img = dicomImage.RenderImage(0); - using var sharpImage = img.AsSharpImage(); - using MemoryStream resultStream = _recyclableMemoryStreamManager.GetStream(); - await sharpImage.SaveAsJpegAsync(resultStream, new SixLabors.ImageSharp.Formats.Jpeg.JpegEncoder(), DefaultCancellationToken); - resultStream.Position = 0; - AssertStreamsEqual(resultStream, response.ResponseStream); - Assert.Equal("image/jpeg", response.ContentType); - Assert.Equal(1, _dicomRequestContextAccessor.RequestContext.PartCount); - Assert.Equal(resultStream.Length, _dicomRequestContextAccessor.RequestContext.BytesRendered); - - var retrieveRenderedRequest2 = new RetrieveRenderedRequest(_studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, ResourceType.Frames, 2, 75, new[] { AcceptHeaderHelpers.CreateRenderAcceptHeader() }); - - _fileStore.GetFileAsync(versionedInstanceIdentifiers[0].VersionedInstanceIdentifier.Version, versionedInstanceIdentifiers[0].VersionedInstanceIdentifier.Partition, DefaultFileProperties, DefaultCancellationToken).Returns(streamAndStoredFileForFrame2); - RetrieveRenderedResponse response2 = await _retrieveRenderedService.RetrieveRenderedImageAsync( - retrieveRenderedRequest2, - DefaultCancellationToken); - - copyStream.Position = 0; - using var img2 = dicomImage.RenderImage(1); - using var sharpImage2 = img2.AsSharpImage(); - using MemoryStream resultStream2 = _recyclableMemoryStreamManager.GetStream(); - await sharpImage2.SaveAsJpegAsync(resultStream2, new SixLabors.ImageSharp.Formats.Jpeg.JpegEncoder(), DefaultCancellationToken); - resultStream2.Position = 0; - AssertStreamsEqual(resultStream2, response2.ResponseStream); - Assert.Equal("image/jpeg", response.ContentType); - Assert.Equal(1, _dicomRequestContextAccessor.RequestContext.PartCount); - Assert.Equal(resultStream2.Length, _dicomRequestContextAccessor.RequestContext.BytesRendered); - - copyStream.Dispose(); - streamAndStoredFileForFrame2.Dispose(); - response.ResponseStream.Dispose(); - response2.ResponseStream.Dispose(); - - } - - [Fact] - public async Task GivenStoredInstancesWithFramesJpeg_WhenRetrieveRenderedForFramesDifferentQuality_ThenEachFrameRenderedSuccesfully() - { - List versionedInstanceIdentifiers = SetupInstanceIdentifiersList(partition: _dicomRequestContextAccessor.RequestContext.DataPartition); - - KeyValuePair streamAndStoredFile = await RetrieveHelpers.StreamAndStoredFileFromDataset(RetrieveHelpers.GenerateDatasetsFromIdentifiers(versionedInstanceIdentifiers[0].VersionedInstanceIdentifier), _recyclableMemoryStreamManager, frames: 3); - _fileStore.GetFileAsync(versionedInstanceIdentifiers[0].VersionedInstanceIdentifier.Version, versionedInstanceIdentifiers[0].VersionedInstanceIdentifier.Partition, DefaultFileProperties, DefaultCancellationToken).Returns(streamAndStoredFile.Value); - _fileStore.GetFilePropertiesAsync(versionedInstanceIdentifiers[0].VersionedInstanceIdentifier.Version, versionedInstanceIdentifiers[0].VersionedInstanceIdentifier.Partition, DefaultFileProperties, DefaultCancellationToken).Returns(new FileProperties { ContentLength = streamAndStoredFile.Value.Length }); - - JpegEncoder jpegEncoder = new JpegEncoder(); - jpegEncoder.Quality = 50; - streamAndStoredFile.Value.Position = 0; - - MemoryStream copyStream = _recyclableMemoryStreamManager.GetStream(); - await streamAndStoredFile.Value.CopyToAsync(copyStream); - copyStream.Position = 0; - streamAndStoredFile.Value.Position = 0; - - MemoryStream streamAndStoredFileForFrame2 = _recyclableMemoryStreamManager.GetStream(); - await streamAndStoredFile.Value.CopyToAsync(streamAndStoredFileForFrame2); - streamAndStoredFileForFrame2.Position = 0; - streamAndStoredFile.Value.Position = 0; - - var retrieveRenderedRequest = new RetrieveRenderedRequest(_studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, ResourceType.Frames, 1, 50, new[] { AcceptHeaderHelpers.CreateRenderAcceptHeader() }); - - RetrieveRenderedResponse response = await _retrieveRenderedService.RetrieveRenderedImageAsync( - retrieveRenderedRequest, - DefaultCancellationToken); - - DicomFile dicomFile = await DicomFile.OpenAsync(copyStream, FileReadOption.ReadLargeOnDemand); - DicomImage dicomImage = new DicomImage(dicomFile.Dataset); - using var img = dicomImage.RenderImage(0); - using var sharpImage = img.AsSharpImage(); - using MemoryStream resultStream = _recyclableMemoryStreamManager.GetStream(); - await sharpImage.SaveAsJpegAsync(resultStream, jpegEncoder, DefaultCancellationToken); - resultStream.Position = 0; - AssertStreamsEqual(resultStream, response.ResponseStream); - Assert.Equal("image/jpeg", response.ContentType); - Assert.Equal(1, _dicomRequestContextAccessor.RequestContext.PartCount); - Assert.Equal(resultStream.Length, _dicomRequestContextAccessor.RequestContext.BytesRendered); - - var retrieveRenderedRequest2 = new RetrieveRenderedRequest(_studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, ResourceType.Frames, 2, 20, new[] { AcceptHeaderHelpers.CreateRenderAcceptHeader() }); - - _fileStore.GetFileAsync(versionedInstanceIdentifiers[0].VersionedInstanceIdentifier.Version, versionedInstanceIdentifiers[0].VersionedInstanceIdentifier.Partition, DefaultFileProperties, DefaultCancellationToken).Returns(streamAndStoredFileForFrame2); - RetrieveRenderedResponse response2 = await _retrieveRenderedService.RetrieveRenderedImageAsync( - retrieveRenderedRequest2, - DefaultCancellationToken); - - copyStream.Position = 0; - using var img2 = dicomImage.RenderImage(1); - using var sharpImage2 = img2.AsSharpImage(); - using MemoryStream resultStream2 = _recyclableMemoryStreamManager.GetStream(); - jpegEncoder.Quality = 20; - await sharpImage2.SaveAsJpegAsync(resultStream2, jpegEncoder, DefaultCancellationToken); - resultStream2.Position = 0; - AssertStreamsEqual(resultStream2, response2.ResponseStream); - Assert.Equal("image/jpeg", response.ContentType); - Assert.Equal(1, _dicomRequestContextAccessor.RequestContext.PartCount); - Assert.Equal(resultStream2.Length, _dicomRequestContextAccessor.RequestContext.BytesRendered); - - copyStream.Dispose(); - streamAndStoredFileForFrame2.Dispose(); - response.ResponseStream.Dispose(); - response2.ResponseStream.Dispose(); - } - - [Fact] - public async Task GivenStoredInstancesWithFramesPNG_WhenRetrieveRenderedForFrames_ThenEachFrameRenderedSuccesfully() - { - List versionedInstanceIdentifiers = SetupInstanceIdentifiersList(partition: _dicomRequestContextAccessor.RequestContext.DataPartition); - - KeyValuePair streamAndStoredFile = await RetrieveHelpers.StreamAndStoredFileFromDataset(RetrieveHelpers.GenerateDatasetsFromIdentifiers(versionedInstanceIdentifiers[0].VersionedInstanceIdentifier), _recyclableMemoryStreamManager, frames: 3); - _fileStore.GetFileAsync(versionedInstanceIdentifiers[0].VersionedInstanceIdentifier.Version, versionedInstanceIdentifiers[0].VersionedInstanceIdentifier.Partition, DefaultFileProperties, DefaultCancellationToken).Returns(streamAndStoredFile.Value); - _fileStore.GetFilePropertiesAsync(versionedInstanceIdentifiers[0].VersionedInstanceIdentifier.Version, versionedInstanceIdentifiers[0].VersionedInstanceIdentifier.Partition, DefaultFileProperties, DefaultCancellationToken).Returns(new FileProperties { ContentLength = streamAndStoredFile.Value.Length }); - - MemoryStream copyStream = _recyclableMemoryStreamManager.GetStream(); - await streamAndStoredFile.Value.CopyToAsync(copyStream); - copyStream.Position = 0; - streamAndStoredFile.Value.Position = 0; - - MemoryStream streamAndStoredFileForFrame2 = _recyclableMemoryStreamManager.GetStream(); - await streamAndStoredFile.Value.CopyToAsync(streamAndStoredFileForFrame2); - streamAndStoredFileForFrame2.Position = 0; - streamAndStoredFile.Value.Position = 0; - - var retrieveRenderedRequest = new RetrieveRenderedRequest(_studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, ResourceType.Frames, 1, 75, new[] { AcceptHeaderHelpers.CreateRenderAcceptHeader(mediaType: KnownContentTypes.ImagePng) }); - - RetrieveRenderedResponse response = await _retrieveRenderedService.RetrieveRenderedImageAsync( - retrieveRenderedRequest, - DefaultCancellationToken); - - DicomFile dicomFile = await DicomFile.OpenAsync(copyStream, FileReadOption.ReadLargeOnDemand); - DicomImage dicomImage = new DicomImage(dicomFile.Dataset); - using var img = dicomImage.RenderImage(0); - using var sharpImage = img.AsSharpImage(); - using MemoryStream resultStream = _recyclableMemoryStreamManager.GetStream(); - await sharpImage.SaveAsPngAsync(resultStream, new SixLabors.ImageSharp.Formats.Png.PngEncoder(), DefaultCancellationToken); - resultStream.Position = 0; - AssertStreamsEqual(resultStream, response.ResponseStream); - Assert.Equal("image/png", response.ContentType); - Assert.Equal(1, _dicomRequestContextAccessor.RequestContext.PartCount); - Assert.Equal(resultStream.Length, _dicomRequestContextAccessor.RequestContext.BytesRendered); - - var retrieveRenderedRequest2 = new RetrieveRenderedRequest(_studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, ResourceType.Frames, 2, 75, new[] { AcceptHeaderHelpers.CreateRenderAcceptHeader(mediaType: KnownContentTypes.ImagePng) }); - - _fileStore.GetFileAsync(versionedInstanceIdentifiers[0].VersionedInstanceIdentifier.Version, versionedInstanceIdentifiers[0].VersionedInstanceIdentifier.Partition, DefaultFileProperties, DefaultCancellationToken).Returns(streamAndStoredFileForFrame2); - RetrieveRenderedResponse response2 = await _retrieveRenderedService.RetrieveRenderedImageAsync( - retrieveRenderedRequest2, - DefaultCancellationToken); - - copyStream.Position = 0; - using var img2 = dicomImage.RenderImage(1); - using var sharpImage2 = img2.AsSharpImage(); - using MemoryStream resultStream2 = _recyclableMemoryStreamManager.GetStream(); - await sharpImage2.SaveAsPngAsync(resultStream2, new SixLabors.ImageSharp.Formats.Png.PngEncoder(), DefaultCancellationToken); - resultStream2.Position = 0; - AssertStreamsEqual(resultStream2, response2.ResponseStream); - Assert.Equal("image/png", response.ContentType); - Assert.Equal(1, _dicomRequestContextAccessor.RequestContext.PartCount); - Assert.Equal(resultStream2.Length, _dicomRequestContextAccessor.RequestContext.BytesRendered); - - copyStream.Dispose(); - streamAndStoredFileForFrame2.Dispose(); - response.ResponseStream.Dispose(); - response2.ResponseStream.Dispose(); - - } - - [Fact] - public async Task GivenStoredInstances_WhenRetrieveRenderedWithoutSpecifyingAcceptHeaders_ThenRenderJpegSuccesfully() - { - List versionedInstanceIdentifiers = SetupInstanceIdentifiersList(partition: _dicomRequestContextAccessor.RequestContext.DataPartition); - - KeyValuePair streamAndStoredFile = await RetrieveHelpers.StreamAndStoredFileFromDataset(RetrieveHelpers.GenerateDatasetsFromIdentifiers(versionedInstanceIdentifiers[0].VersionedInstanceIdentifier), _recyclableMemoryStreamManager, frames: 3); - - MemoryStream copyStream = _recyclableMemoryStreamManager.GetStream(); - await streamAndStoredFile.Value.CopyToAsync(copyStream); - copyStream.Position = 0; - - streamAndStoredFile.Value.Position = 0; - - _fileStore.GetFileAsync(versionedInstanceIdentifiers[0].VersionedInstanceIdentifier.Version, versionedInstanceIdentifiers[0].VersionedInstanceIdentifier.Partition, DefaultFileProperties, DefaultCancellationToken).Returns(streamAndStoredFile.Value); - _fileStore.GetFilePropertiesAsync(versionedInstanceIdentifiers[0].VersionedInstanceIdentifier.Version, versionedInstanceIdentifiers[0].VersionedInstanceIdentifier.Partition, DefaultFileProperties, DefaultCancellationToken).Returns(new FileProperties { ContentLength = streamAndStoredFile.Value.Length }); - - var retrieveRenderedRequest = new RetrieveRenderedRequest(_studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, ResourceType.Instance, 1, 75, new List()); - - RetrieveRenderedResponse response = await _retrieveRenderedService.RetrieveRenderedImageAsync( - retrieveRenderedRequest, - DefaultCancellationToken); - - DicomFile dicomFile = await DicomFile.OpenAsync(copyStream, FileReadOption.ReadLargeOnDemand); - DicomImage dicomImage = new DicomImage(dicomFile.Dataset); - using var img = dicomImage.RenderImage(0); - using var sharpImage = img.AsSharpImage(); - using MemoryStream resultStream = _recyclableMemoryStreamManager.GetStream(); - await sharpImage.SaveAsJpegAsync(resultStream, new SixLabors.ImageSharp.Formats.Jpeg.JpegEncoder(), DefaultCancellationToken); - resultStream.Position = 0; - AssertStreamsEqual(resultStream, response.ResponseStream); - Assert.Equal("image/jpeg", response.ContentType); - Assert.Equal(1, _dicomRequestContextAccessor.RequestContext.PartCount); - Assert.Equal(resultStream.Length, _dicomRequestContextAccessor.RequestContext.BytesRendered); - - response.ResponseStream.Dispose(); - copyStream.Dispose(); - } - - private List SetupInstanceIdentifiersList(Partition partition = null, InstanceProperties instanceProperty = null) - { - var dicomInstanceIdentifiersList = new List(); - - instanceProperty ??= new InstanceProperties { FileProperties = DefaultFileProperties }; - partition ??= Partition.Default; - - dicomInstanceIdentifiersList.Add(new InstanceMetadata(new VersionedInstanceIdentifier(_studyInstanceUid, _firstSeriesInstanceUid, TestUidGenerator.Generate(), 0, partition), instanceProperty)); - _instanceStore.GetInstanceIdentifierWithPropertiesAsync(dicomInstanceIdentifiersList[0].VersionedInstanceIdentifier.Partition, _studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, false, DefaultCancellationToken).Returns(dicomInstanceIdentifiersList); - - return dicomInstanceIdentifiersList; - } - - private static void AssertStreamsEqual(Stream expectedPixelData, Stream actualPixelData) - { - Assert.Equal(expectedPixelData.Length, actualPixelData.Length); - Assert.Equal(0, actualPixelData.Position); - Assert.Equal(0, expectedPixelData.Position); - for (var i = 0; i < expectedPixelData.Length; i++) - { - Assert.Equal(expectedPixelData.ReadByte(), actualPixelData.ReadByte()); - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Retrieve/RetrieveResourceHandlerTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Retrieve/RetrieveResourceHandlerTests.cs deleted file mode 100644 index f3a29cea4d..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Retrieve/RetrieveResourceHandlerTests.cs +++ /dev/null @@ -1,175 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.Health.Core.Features.Security.Authorization; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Retrieve; -using Microsoft.Health.Dicom.Core.Features.Security; -using Microsoft.Health.Dicom.Core.Messages.Retrieve; -using Microsoft.Health.Dicom.Core.Web; -using Microsoft.Health.Dicom.Tests.Common; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Retrieve; - -public class RetrieveResourceHandlerTests -{ - private readonly IRetrieveResourceService _retrieveResourceService; - private readonly RetrieveResourceHandler _retrieveResourceHandler; - - public RetrieveResourceHandlerTests() - { - _retrieveResourceService = Substitute.For(); - _retrieveResourceHandler = new RetrieveResourceHandler(new DisabledAuthorizationService(), _retrieveResourceService); - } - - [Theory] - [InlineData("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")] - [InlineData("345%^&")] - public async Task GivenARequestWithInvalidIdentifier_WhenRetrievingStudy_ThenDicomInvalidIdentifierExceptionIsThrown(string studyInstanceUid) - { - EnsureArg.IsNotNull(studyInstanceUid, nameof(studyInstanceUid)); - RetrieveResourceRequest request = new RetrieveResourceRequest(studyInstanceUid, new[] { AcceptHeaderHelpers.CreateAcceptHeaderForGetInstance() }); - await Assert.ThrowsAsync(() => _retrieveResourceHandler.Handle(request, CancellationToken.None)); - } - - [Theory] - [InlineData("aaaa-bbbb", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")] - [InlineData("aaaa-bbbb", " ")] - [InlineData("aaaa-bbbb", "345%^&")] - [InlineData("aaaa-bbbb", "aaaa-bbbb")] - public async Task GivenARequestWithInvalidStudyAndSeriesIdentifiers_WhenRetrievingSeries_ThenDicomInvalidIdentifierExceptionIsThrown(string studyInstanceUid, string seriesInstanceUid) - { - EnsureArg.IsNotNull(studyInstanceUid, nameof(studyInstanceUid)); - RetrieveResourceRequest request = new RetrieveResourceRequest(studyInstanceUid, seriesInstanceUid, new[] { AcceptHeaderHelpers.CreateAcceptHeaderForGetSeries() }); - await Assert.ThrowsAsync(() => _retrieveResourceHandler.Handle(request, CancellationToken.None)); - } - - [Theory] - [InlineData("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")] - [InlineData("345%^&")] - [InlineData("aaaa-bbbb")] - [InlineData("()")] - public async Task GivenARequestWithInvalidSeriesIdentifier_WhenRetrievingSeries_ThenDicomInvalidIdentifierExceptionIsThrown(string seriesInstanceUid) - { - EnsureArg.IsNotNull(seriesInstanceUid, nameof(seriesInstanceUid)); - RetrieveResourceRequest request = new RetrieveResourceRequest(TestUidGenerator.Generate(), seriesInstanceUid, new[] { AcceptHeaderHelpers.CreateAcceptHeaderForGetSeries() }); - await Assert.ThrowsAsync(() => _retrieveResourceHandler.Handle(request, CancellationToken.None)); - } - - [Theory] - [InlineData("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")] - [InlineData("345%^&")] - [InlineData("aaaa-bbbb")] - [InlineData("()")] - public async Task GivenARequestWithInvalidInstanceIdentifier_WhenRetrievingInstance_ThenDicomInvalidIdentifierExceptionIsThrown(string sopInstanceUid) - { - EnsureArg.IsNotNull(sopInstanceUid, nameof(sopInstanceUid)); - RetrieveResourceRequest request = new RetrieveResourceRequest(TestUidGenerator.Generate(), TestUidGenerator.Generate(), sopInstanceUid, new[] { AcceptHeaderHelpers.CreateAcceptHeaderForGetInstance() }); - await Assert.ThrowsAsync(() => _retrieveResourceHandler.Handle(request, CancellationToken.None)); - } - - [Theory] - [InlineData("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")] - [InlineData("345%^&")] - [InlineData("aaaa-bbbb")] - [InlineData("()")] - public async Task GivenARequestWithInvalidInstanceIdentifier_WhenRetrievingFrames_ThenDicomInvalidIdentifierExceptionIsThrown(string sopInstanceUid) - { - EnsureArg.IsNotNull(sopInstanceUid, nameof(sopInstanceUid)); - RetrieveResourceRequest request = new RetrieveResourceRequest(TestUidGenerator.Generate(), TestUidGenerator.Generate(), sopInstanceUid, new List { 1 }, new[] { AcceptHeaderHelpers.CreateAcceptHeaderForGetFrame() }); - await Assert.ThrowsAsync(() => _retrieveResourceHandler.Handle(request, CancellationToken.None)); - } - - [Theory(Skip = "Move this tests to move this tests to RetriveResourceService, since the logic to validate TransferSyntax has moved there")] - [InlineData("*-")] - [InlineData("invalid")] - [InlineData("00000000000000000000000000000000000000000000000000000000000000065")] - public async Task GivenIncorrectTransferSyntax_WhenRetrievingStudy_ThenDicomBadRequestExceptionIsThrownAsync(string transferSyntax) - { - var request = new RetrieveResourceRequest(TestUidGenerator.Generate(), new[] { AcceptHeaderHelpers.CreateAcceptHeaderForGetInstance(transferSyntax: transferSyntax) }); - - var ex = await Assert.ThrowsAsync(() => _retrieveResourceHandler.Handle(request, CancellationToken.None)); - - Assert.Equal("The specified Transfer Syntax value is not valid.", ex.Message); - } - - [Theory] - [InlineData(0)] - [InlineData(-1)] - [InlineData(-234)] - public async Task GivenInvalidFrameNumber_WhenRetrievingFrames_ThenDicomBadRequestExceptionIsThrownAsync(int frame) - { - const string expectedErrorMessage = "The specified frames value is not valid. At least one frame must be present, and all requested frames must have value greater than 0."; - var request = new RetrieveResourceRequest( - studyInstanceUid: TestUidGenerator.Generate(), - seriesInstanceUid: TestUidGenerator.Generate(), - sopInstanceUid: TestUidGenerator.Generate(), - frames: new[] { frame }, - acceptHeaders: new[] { AcceptHeaderHelpers.CreateAcceptHeaderForGetFrame() }); - - var ex = await Assert.ThrowsAsync(() => _retrieveResourceHandler.Handle(request, CancellationToken.None)); - - Assert.Equal(expectedErrorMessage, ex.Message); - } - - [Theory] - [InlineData(null)] - [InlineData(new int[0])] - public async Task GivenNoFrames_WhenRetrievingFrames_ThenDicomBadRequestExceptionIsThrownAsync(int[] frames) - { - const string expectedErrorMessage = "The specified frames value is not valid. At least one frame must be present, and all requested frames must have value greater than 0."; - var request = new RetrieveResourceRequest( - studyInstanceUid: TestUidGenerator.Generate(), - seriesInstanceUid: TestUidGenerator.Generate(), - sopInstanceUid: TestUidGenerator.Generate(), - frames: frames, - acceptHeaders: new[] { AcceptHeaderHelpers.CreateAcceptHeaderForGetFrame() }); - - var ex = await Assert.ThrowsAsync(() => _retrieveResourceHandler.Handle(request, CancellationToken.None)); - - Assert.Equal(expectedErrorMessage, ex.Message); - } - - [Theory] - [InlineData("1", "1", "2")] - [InlineData("1", "2", "1")] - [InlineData("1", "2", "2")] - public async Task GivenRepeatedIdentifiers_WhenRetrievingFrames_ThenNoExceptionIsThrown( - string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid) - { - RetrieveResourceResponse expectedResponse = new RetrieveResourceResponse(Substitute.For>(), KnownContentTypes.ApplicationOctetStream); - var request = new RetrieveResourceRequest( - studyInstanceUid: studyInstanceUid, - seriesInstanceUid: seriesInstanceUid, - sopInstanceUid: sopInstanceUid, - frames: new int[] { 1 }, - acceptHeaders: new[] { AcceptHeaderHelpers.CreateAcceptHeaderForGetFrame() }); - _retrieveResourceService.GetInstanceResourceAsync(request, CancellationToken.None).Returns(expectedResponse); - - RetrieveResourceResponse response = await _retrieveResourceHandler.Handle(request, CancellationToken.None); - Assert.Same(expectedResponse, response); - } - - [Fact] - public async Task GivenARequestWithValidInstanceIdentifier_WhenRetrievingFrames_ThenNoExceptionIsThrown() - { - string studyInstanceUid = TestUidGenerator.Generate(); - string seriesInstanceUid = TestUidGenerator.Generate(); - string sopInstanceUid = TestUidGenerator.Generate(); - - RetrieveResourceResponse expectedResponse = new RetrieveResourceResponse(Substitute.For>(), KnownContentTypes.ApplicationOctetStream); - RetrieveResourceRequest request = new RetrieveResourceRequest(studyInstanceUid, seriesInstanceUid, sopInstanceUid, new List { 1 }, new[] { AcceptHeaderHelpers.CreateAcceptHeaderForGetFrame() }); - _retrieveResourceService.GetInstanceResourceAsync(request, CancellationToken.None).Returns(expectedResponse); - - RetrieveResourceResponse response = await _retrieveResourceHandler.Handle(request, CancellationToken.None); - Assert.Same(expectedResponse, response); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Retrieve/RetrieveResourceServiceTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Retrieve/RetrieveResourceServiceTests.cs deleted file mode 100644 index 679382a3d8..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Retrieve/RetrieveResourceServiceTests.cs +++ /dev/null @@ -1,935 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using FellowOakDicom; -using FellowOakDicom.Imaging; -using FellowOakDicom.IO.Buffer; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Context; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Core.Features.Retrieve; -using Microsoft.Health.Dicom.Core.Features.Telemetry; -using Microsoft.Health.Dicom.Core.Messages; -using Microsoft.Health.Dicom.Core.Messages.Retrieve; -using Microsoft.Health.Dicom.Core.Web; -using Microsoft.Health.Dicom.Tests.Common; -using Microsoft.Health.Dicom.Tests.Common.Extensions; -using Microsoft.IO; -using NSubstitute; -using NSubstitute.ExceptionExtensions; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Retrieve; - -public class RetrieveResourceServiceTests -{ - private readonly IMetadataStore _metadataStore; - private readonly RetrieveResourceService _retrieveResourceService; - private readonly IInstanceStore _instanceStore; - private readonly IFileStore _fileStore; - private readonly ITranscoder _retrieveTranscoder; - private readonly IFrameHandler _dicomFrameHandler; - private readonly IAcceptHeaderHandler _acceptHeaderHandler; - private readonly IDicomRequestContextAccessor _dicomRequestContextAccessor; - private readonly ILogger _logger; - private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager; - - private readonly string _studyInstanceUid = TestUidGenerator.Generate(); - private readonly string _firstSeriesInstanceUid = TestUidGenerator.Generate(); - private readonly string _secondSeriesInstanceUid = TestUidGenerator.Generate(); - private readonly string _sopInstanceUid = TestUidGenerator.Generate(); - private static readonly CancellationToken DefaultCancellationToken = new CancellationTokenSource().Token; - private readonly IInstanceMetadataCache _instanceMetadataCache; - private readonly IFramesRangeCache _framesRangeCache; - private readonly RetrieveMeter _retrieveMeter; - private static readonly FileProperties DefaultFileProperties = new FileProperties() { Path = "default/path/0.dcm", ETag = "123", ContentLength = 123 }; - - public RetrieveResourceServiceTests() - { - _instanceStore = Substitute.For(); - _fileStore = Substitute.For(); - _retrieveTranscoder = Substitute.For(); - _dicomFrameHandler = Substitute.For(); - _acceptHeaderHandler = new AcceptHeaderHandler(NullLogger.Instance); - _logger = NullLogger.Instance; - _recyclableMemoryStreamManager = new RecyclableMemoryStreamManager(); - _dicomRequestContextAccessor = Substitute.For(); - _dicomRequestContextAccessor.RequestContext.DataPartition = Partition.Default; - var retrieveConfigurationSnapshot = Substitute.For>(); - retrieveConfigurationSnapshot.Value.Returns(new RetrieveConfiguration()); - _instanceMetadataCache = Substitute.For(); - _framesRangeCache = Substitute.For(); - _retrieveMeter = new RetrieveMeter(); - - _metadataStore = Substitute.For(); - _retrieveResourceService = new RetrieveResourceService( - _instanceStore, - _fileStore, - _retrieveTranscoder, - _dicomFrameHandler, - _acceptHeaderHandler, - _dicomRequestContextAccessor, - _metadataStore, - _instanceMetadataCache, - _framesRangeCache, - retrieveConfigurationSnapshot, - _retrieveMeter, - _logger); - } - - [Fact] - public async Task GivenNoStoredInstances_WhenRetrieveRequestForStudy_ThenNotFoundIsThrown() - { - _instanceStore.GetInstanceIdentifiersInStudyAsync(Partition.Default, _studyInstanceUid).Returns(new List()); - - await Assert.ThrowsAsync(() => _retrieveResourceService.GetInstanceResourceAsync( - new RetrieveResourceRequest(_studyInstanceUid, new[] { AcceptHeaderHelpers.CreateAcceptHeaderForGetStudy() }), - DefaultCancellationToken)); - } - - [Fact] - public async Task GivenStoredInstancesWhereOneIsMissingFile_WhenRetrieveRequestForStudy_ThenNotFoundIsThrown() - { - List instances = SetupInstanceIdentifiersList(ResourceType.Study); - _fileStore.GetFilePropertiesAsync(Arg.Any(), Partition.Default, null, DefaultCancellationToken).Returns(new FileProperties { ContentLength = 1000 }); - - // For each instance identifier but the last, set up the fileStore to return a stream containing a file associated with the identifier. - // For each instance identifier but the last, set up the fileStore to return a stream containing a file associated with the identifier. - for (int i = 0; i < instances.Count - 1; i++) - { - InstanceMetadata instance = instances[i]; - KeyValuePair stream = await RetrieveHelpers.StreamAndStoredFileFromDataset(RetrieveHelpers.GenerateDatasetsFromIdentifiers(instance.VersionedInstanceIdentifier), _recyclableMemoryStreamManager, frames: 0, disposeStreams: false); - _fileStore.GetStreamingFileAsync(instance.VersionedInstanceIdentifier.Version, instance.VersionedInstanceIdentifier.Partition, null, DefaultCancellationToken).Returns(stream.Value); - _retrieveTranscoder.TranscodeFileAsync(stream.Value, "*").Returns(stream.Value); - } - - // For the last identifier, set up the fileStore to throw a store exception with the status code 404 (NotFound). - _fileStore.GetStreamingFileAsync(instances.Last().VersionedInstanceIdentifier.Version, instances.Last().VersionedInstanceIdentifier.Partition, null, DefaultCancellationToken).Throws(new InstanceNotFoundException()); - - var response = await _retrieveResourceService.GetInstanceResourceAsync( - new RetrieveResourceRequest(_studyInstanceUid, new[] { AcceptHeaderHelpers.CreateAcceptHeaderForGetStudy() }), - DefaultCancellationToken); - await Assert.ThrowsAsync(() => response.GetStreamsAsync()); - } - - [Fact] - public async Task GivenStoredInstances_WhenRetrieveRequestForStudy_ThenInstancesInStudyAreRetrievedSuccesfully() - { - // Add multiple instances to validate that we return the requested instance and ignore the other(s). - List versionedInstanceIdentifiers = SetupInstanceIdentifiersList(ResourceType.Study); - - // For each instance identifier, set up the fileStore to return a stream containing a file associated with the identifier. - var streamsAndStoredFiles = await Task.WhenAll(versionedInstanceIdentifiers.Select( - x => RetrieveHelpers.StreamAndStoredFileFromDataset(RetrieveHelpers.GenerateDatasetsFromIdentifiers(x.VersionedInstanceIdentifier), _recyclableMemoryStreamManager))); - - _fileStore.GetFilePropertiesAsync(Arg.Any(), Partition.Default, null, DefaultCancellationToken).Returns(new FileProperties { ContentLength = 1000 }); - int count = 0; - foreach (var file in streamsAndStoredFiles) - { - _fileStore.GetStreamingFileAsync(count, Partition.Default, null, DefaultCancellationToken).Returns(file.Value); - count++; - } - - RetrieveResourceResponse response = await _retrieveResourceService.GetInstanceResourceAsync( - new RetrieveResourceRequest(_studyInstanceUid, new[] { AcceptHeaderHelpers.CreateAcceptHeaderForGetStudy() }), - DefaultCancellationToken); - - // Validate response status code and ensure response streams have expected files - they should be equivalent to what the store was set up to return. - ValidateResponseStreams(streamsAndStoredFiles.Select(x => x.Key), await response.GetStreamsAsync()); - - // Validate dicom request is populated with correct transcode values - ValidateDicomRequestIsPopulated(); - - // Dispose created streams. - streamsAndStoredFiles.ToList().ForEach(x => x.Value.Dispose()); - - // Validate instance count is added to dicom request context - Assert.Equal(streamsAndStoredFiles.Length, _dicomRequestContextAccessor.RequestContext.PartCount); - } - - [Fact] - public async Task GivenStoredInstancesWithOriginalVersion_WhenRetrieveRequestForStudyForOriginal_ThenInstancesInStudyAreRetrievedSuccesfully() - { - // Add multiple instances to validate that we return the requested instance and ignore the other(s). - List versionedInstanceIdentifiers = SetupInstanceIdentifiersList( - ResourceType.Study, - instanceProperty: new InstanceProperties() { HasFrameMetadata = true, OriginalVersion = 5 }, - isOriginalVersion: true); - - _fileStore.GetFilePropertiesAsync(Arg.Any(), Partition.Default, null, DefaultCancellationToken).Returns(new FileProperties { ContentLength = 1000 }); - - RetrieveResourceResponse response = await _retrieveResourceService.GetInstanceResourceAsync( - new RetrieveResourceRequest(_studyInstanceUid, new[] { AcceptHeaderHelpers.CreateAcceptHeaderForGetStudy() }, isOriginalVersionRequested: true), - DefaultCancellationToken); - - await response.GetStreamsAsync(); - - await _fileStore - .Received(3) - .GetStreamingFileAsync(Arg.Is(x => x == 5), Partition.Default, null, DefaultCancellationToken); - } - - [Fact] - public async Task GivenStoredInstancesWithOriginalVersion_WhenRetrieveRequestForStudyForLatest_ThenInstancesInStudyAreRetrievedSuccesfully() - { - // Add multiple instances to validate that we return the requested instance and ignore the other(s). - List versionedInstanceIdentifiers = SetupInstanceIdentifiersList( - ResourceType.Study, - instanceProperty: new InstanceProperties() { HasFrameMetadata = true, OriginalVersion = 5 }); - - _fileStore.GetFilePropertiesAsync(Arg.Any(), Partition.Default, null, DefaultCancellationToken).Returns(new FileProperties { ContentLength = 1000 }); - - RetrieveResourceResponse response = await _retrieveResourceService.GetInstanceResourceAsync( - new RetrieveResourceRequest(_studyInstanceUid, new[] { AcceptHeaderHelpers.CreateAcceptHeaderForGetStudy() }), - DefaultCancellationToken); - - await response.GetStreamsAsync(); - - await _fileStore - .Received(1) - .GetStreamingFileAsync(Arg.Is(x => x == 0), Partition.Default, null, DefaultCancellationToken); - } - - [Fact] - public async Task GivenInstancesWithOriginalVersion_WhenRetrieveRequestForStudyForOriginalWithTranscoding_ThenInstancesAreReturned() - { - // Add multiple instances to validate that we return the requested instance and ignore the other(s). - int originalVersion = 5; - FileProperties fileProperties = new FileProperties { Path = "123.dcm", ETag = "e456", ContentLength = 123 }; - List versionedInstanceIdentifiers = SetupInstanceIdentifiersList( - ResourceType.Instance, - instanceProperty: new InstanceProperties() - { - HasFrameMetadata = true, - OriginalVersion = originalVersion, - TransferSyntaxUid = "1.2.840.10008.1.2.4.90", - FileProperties = fileProperties - }, - isOriginalVersion: true); - _fileStore.GetFilePropertiesAsync(originalVersion, versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Partition, fileProperties, DefaultCancellationToken) - .Returns(new FileProperties { ContentLength = new RetrieveConfiguration().MaxDicomFileSize }); - - // For each instance identifier, set up the fileStore to return a stream containing a file associated with the identifier. - var streamsAndStoredFiles = await Task.WhenAll(versionedInstanceIdentifiers.Select( - x => RetrieveHelpers.StreamAndStoredFileFromDataset(RetrieveHelpers.GenerateDatasetsFromIdentifiers(x.VersionedInstanceIdentifier), _recyclableMemoryStreamManager))); - - _retrieveTranscoder.TranscodeFileAsync(Arg.Any(), Arg.Any()).Returns(streamsAndStoredFiles.First().Value); - - RetrieveResourceResponse response = await _retrieveResourceService.GetInstanceResourceAsync( - new RetrieveResourceRequest( - _studyInstanceUid, - _firstSeriesInstanceUid, - _sopInstanceUid, - new[] { AcceptHeaderHelpers.CreateAcceptHeaderForGetStudy("1.2.840.10008.1.2.1") }, - isOriginalVersionRequested: true), - DefaultCancellationToken); - - await response.GetStreamsAsync(); - - await _fileStore - .Received(1) - .GetFilePropertiesAsync(Arg.Is(x => x == originalVersion), versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Partition, versionedInstanceIdentifiers.First().InstanceProperties.FileProperties, DefaultCancellationToken); - - await _fileStore - .DidNotReceive() - .GetStreamingFileAsync(Arg.Any(), versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Partition, null, DefaultCancellationToken); - - await _fileStore - .Received(1) - .GetFileAsync(Arg.Is(x => x == originalVersion), versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Partition, fileProperties, DefaultCancellationToken); - } - - [Fact] - public async Task GivenSpecificTransferSyntax_WhenRetrieveRequestForStudy_ThenInstancesInStudyAreTranscodedSuccesfully() - { - // Add multiple instances to validate that we return the requested instance and ignore the other(s). - var instanceMetadata = new InstanceMetadata(new VersionedInstanceIdentifier(_studyInstanceUid, _firstSeriesInstanceUid, TestUidGenerator.Generate(), 0, Partition.Default), new InstanceProperties()); - _instanceStore.GetInstanceIdentifierWithPropertiesAsync(_dicomRequestContextAccessor.RequestContext.DataPartition, _studyInstanceUid, null, null, false, DefaultCancellationToken).Returns(new[] { instanceMetadata }); - - // For each instance identifier, set up the fileStore to return a stream containing a file associated with the identifier. - var streamsAndStoredFile = await RetrieveHelpers.StreamAndStoredFileFromDataset(RetrieveHelpers.GenerateDatasetsFromIdentifiers(instanceMetadata.VersionedInstanceIdentifier), _recyclableMemoryStreamManager); - - _fileStore.GetFileAsync(0, _dicomRequestContextAccessor.RequestContext.DataPartition, null, DefaultCancellationToken).Returns(streamsAndStoredFile.Value); - _fileStore.GetFilePropertiesAsync(instanceMetadata.VersionedInstanceIdentifier.Version, _dicomRequestContextAccessor.RequestContext.DataPartition, instanceMetadata.InstanceProperties.FileProperties, DefaultCancellationToken).Returns(new FileProperties { ContentLength = streamsAndStoredFile.Value.Length }); - string transferSyntax = "1.2.840.10008.1.2.1"; - _retrieveTranscoder.TranscodeFileAsync(streamsAndStoredFile.Value, transferSyntax).Returns(CopyStream(streamsAndStoredFile.Value)); - - RetrieveResourceResponse response = await _retrieveResourceService.GetInstanceResourceAsync( - new RetrieveResourceRequest(_studyInstanceUid, new[] { AcceptHeaderHelpers.CreateAcceptHeaderForGetStudy(transferSyntax: transferSyntax) }), - DefaultCancellationToken); - - // Validate response status code and ensure response streams have expected files - they should be equivalent to what the store was set up to return. - ValidateResponseStreams(new[] { streamsAndStoredFile.Key }, await response.GetStreamsAsync()); - - // Validate dicom request is populated with correct transcode values - IEnumerable streams = await response.GetStreamsAsync(); - ValidateDicomRequestIsPopulated(true, streams.Sum(s => s.Length)); - - // Dispose created streams. - streamsAndStoredFile.Value.Dispose(); - } - - [Fact] - public async Task GivenNoStoredInstances_WhenRetrieveRequestForSeries_ThenNotFoundIsThrown() - { - _instanceStore.GetInstanceIdentifiersInSeriesAsync(Partition.Default, _studyInstanceUid, _firstSeriesInstanceUid).Returns(new List()); - - await Assert.ThrowsAsync(() => _retrieveResourceService.GetInstanceResourceAsync( - new RetrieveResourceRequest(_studyInstanceUid, _firstSeriesInstanceUid, new[] { AcceptHeaderHelpers.CreateAcceptHeaderForGetSeries() }), - DefaultCancellationToken)); - } - - [Fact] - public async Task GivenStoredInstancesWhereOneIsMissingFile_WhenRetrieveRequestForSeries_ThenNotFoundIsThrown() - { - List versionedInstanceIdentifiers = SetupInstanceIdentifiersList(ResourceType.Series); - _fileStore.GetFilePropertiesAsync(Arg.Any(), Partition.Default, null, DefaultCancellationToken).Returns(new FileProperties { ContentLength = 1000 }); - - // For each instance identifier but the last, set up the fileStore to return a stream containing a file associated with the identifier. - for (int i = 0; i < versionedInstanceIdentifiers.Count - 1; i++) - { - InstanceMetadata instance = versionedInstanceIdentifiers[i]; - KeyValuePair stream = await RetrieveHelpers.StreamAndStoredFileFromDataset(RetrieveHelpers.GenerateDatasetsFromIdentifiers(instance.VersionedInstanceIdentifier), _recyclableMemoryStreamManager, frames: 0, disposeStreams: false); - _fileStore.GetStreamingFileAsync(instance.VersionedInstanceIdentifier.Version, instance.VersionedInstanceIdentifier.Partition, null, DefaultCancellationToken).Returns(stream.Value); - _retrieveTranscoder.TranscodeFileAsync(stream.Value, "*").Returns(stream.Value); - } - - // For the last identifier, set up the fileStore to throw a store exception with the status code 404 (NotFound). - _fileStore.GetStreamingFileAsync(versionedInstanceIdentifiers.Last().VersionedInstanceIdentifier.Version, versionedInstanceIdentifiers.Last().VersionedInstanceIdentifier.Partition, null, DefaultCancellationToken).Throws(new InstanceNotFoundException()); - - var response = await _retrieveResourceService.GetInstanceResourceAsync( - new RetrieveResourceRequest(_studyInstanceUid, _firstSeriesInstanceUid, new[] { AcceptHeaderHelpers.CreateAcceptHeaderForGetSeries() }), - DefaultCancellationToken); - await Assert.ThrowsAsync(() => response.GetStreamsAsync()); - } - - [Fact] - public async Task GivenStoredInstances_WhenRetrieveRequestForSeries_ThenInstancesInSeriesAreRetrievedSuccesfully() - { - // Add multiple instances to validate that we return the requested instance and ignore the other(s). - List versionedInstanceIdentifiers = SetupInstanceIdentifiersList(ResourceType.Series); - - // For each instance identifier, set up the fileStore to return a stream containing a file associated with the identifier. - var streamsAndStoredFiles = await Task.WhenAll(versionedInstanceIdentifiers - .Select(x => RetrieveHelpers.StreamAndStoredFileFromDataset(RetrieveHelpers.GenerateDatasetsFromIdentifiers(x.VersionedInstanceIdentifier), _recyclableMemoryStreamManager))); - - _fileStore.GetFilePropertiesAsync(Arg.Any(), Partition.Default, null, DefaultCancellationToken).Returns(new FileProperties { ContentLength = 1000 }); - int count = 0; - foreach (var file in streamsAndStoredFiles) - { - _fileStore.GetStreamingFileAsync(count, Partition.Default, null, DefaultCancellationToken).Returns(file.Value); - count++; - } - - RetrieveResourceResponse response = await _retrieveResourceService.GetInstanceResourceAsync( - new RetrieveResourceRequest( - _studyInstanceUid, - _firstSeriesInstanceUid, - new[] { AcceptHeaderHelpers.CreateAcceptHeaderForGetSeries() }), - DefaultCancellationToken); - - // Validate response status code and ensure response streams have expected files - they should be equivalent to what the store was set up to return. - ValidateResponseStreams(streamsAndStoredFiles.Select(x => x.Key), await response.GetStreamsAsync()); - - // Validate dicom request is populated with correct transcode values - ValidateDicomRequestIsPopulated(); - - // Dispose created streams. - streamsAndStoredFiles.ToList().ForEach(x => x.Value.Dispose()); - - // Validate instance count is added to dicom request context - Assert.Equal(streamsAndStoredFiles.Length, _dicomRequestContextAccessor.RequestContext.PartCount); - } - - [Fact] - public async Task GivenNoStoredInstances_WhenRetrieveRequestForInstance_ThenNotFoundIsThrown() - { - _instanceStore.GetInstanceIdentifierAsync(Partition.Default, _studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid).Returns(new List()); - - await Assert.ThrowsAsync(() => _retrieveResourceService.GetInstanceResourceAsync( - new RetrieveResourceRequest(_studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, new[] { AcceptHeaderHelpers.CreateAcceptHeaderForGetInstance() }), - DefaultCancellationToken)); - } - - [Fact] - public async Task GivenStoredInstancesWithMissingFile_WhenRetrieveRequestForInstance_ThenNotFoundIsThrown() - { - // Add multiple instances to validate that we return the requested instance and ignore the other(s). - List versionedInstanceIdentifiers = SetupInstanceIdentifiersList(ResourceType.Instance); - _fileStore.GetFilePropertiesAsync(Arg.Any(), Partition.Default, null, DefaultCancellationToken).Returns(new FileProperties { ContentLength = 1000 }); - - // For the first instance identifier, set up the fileStore to throw a store exception with the status code 404 (NotFound). - _fileStore.GetStreamingFileAsync(versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Version, versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Partition, null, DefaultCancellationToken).Throws(new InstanceNotFoundException()); - - var response = await _retrieveResourceService.GetInstanceResourceAsync( - new RetrieveResourceRequest(_studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, new[] { AcceptHeaderHelpers.CreateAcceptHeaderForGetInstance() }), - DefaultCancellationToken); - await Assert.ThrowsAsync(() => response.GetStreamsAsync()); - } - - [Theory] - [InlineData(PayloadTypes.SinglePart, true)] - [InlineData(PayloadTypes.SinglePart, false)] - [InlineData(PayloadTypes.MultipartRelated, true)] - [InlineData(PayloadTypes.MultipartRelated, false)] - public async Task GivenStoredInstances_WhenRetrieveRequestForInstance_ThenInstanceIsRetrievedSuccessfully(PayloadTypes payloadTypes, bool withFileProperties) - { - // Add multiple instances to validate that we return the requested instance and ignore the other(s). - List versionedInstanceIdentifiers = SetupInstanceIdentifiersList(ResourceType.Instance, withFileProperties: withFileProperties); - - // For the first instance identifier, set up the fileStore to return a stream containing a file associated with the identifier. - KeyValuePair streamAndStoredFile = await RetrieveHelpers.StreamAndStoredFileFromDataset(RetrieveHelpers.GenerateDatasetsFromIdentifiers(versionedInstanceIdentifiers.First().VersionedInstanceIdentifier), _recyclableMemoryStreamManager); - - var expectedFileProperties = withFileProperties - ? versionedInstanceIdentifiers.First().InstanceProperties.FileProperties - : null; - - _fileStore.GetStreamingFileAsync(versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Version, versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Partition, expectedFileProperties, DefaultCancellationToken).Returns(streamAndStoredFile.Value); - - _fileStore.GetFilePropertiesAsync(Arg.Any(), Partition.Default, expectedFileProperties, DefaultCancellationToken).Returns(new FileProperties { ContentLength = 1000 }); - - RetrieveResourceResponse response = await _retrieveResourceService.GetInstanceResourceAsync( - new RetrieveResourceRequest( - _studyInstanceUid, - _firstSeriesInstanceUid, - _sopInstanceUid, - new[] { AcceptHeaderHelpers.CreateAcceptHeaderForGetInstance(payloadTypes: payloadTypes) }), - DefaultCancellationToken); - - // Validate response status code and ensure response stream has expected file - it should be equivalent to what the store was set up to return. - ValidateResponseStreams(new List() { streamAndStoredFile.Key }, await response.GetStreamsAsync()); - - // Validate dicom request is populated with correct transcode values - ValidateDicomRequestIsPopulated(); - - // Validate content type - Assert.Equal(KnownContentTypes.ApplicationDicom, response.ContentType); - - await _fileStore.Received(1) - .GetStreamingFileAsync( - versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Version, - versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Partition, - expectedFileProperties, - DefaultCancellationToken); - - // Dispose created streams. - streamAndStoredFile.Value.Dispose(); - - // Validate instance count is added to dicom request context - Assert.Equal(1, _dicomRequestContextAccessor.RequestContext.PartCount); - } - - [Fact] - public async Task GivenStoredInstancesWithoutFrames_WhenRetrieveRequestForFrame_ThenNotFoundIsThrown() - { - // Add multiple instances to validate that we evaluate the requested instance and ignore the other(s). - List versionedInstanceIdentifiers = SetupInstanceIdentifiersList(ResourceType.Frames); - var framesToRequest = new List { 0 }; - - // For the instance, set up the fileStore to return a stream containing the file associated with the identifier with 3 frames. - Stream streamOfStoredFiles = (await RetrieveHelpers.StreamAndStoredFileFromDataset(RetrieveHelpers.GenerateDatasetsFromIdentifiers(versionedInstanceIdentifiers.First().VersionedInstanceIdentifier), _recyclableMemoryStreamManager, frames: 0)).Value; - _fileStore.GetFileAsync(versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Version, versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Partition, null, DefaultCancellationToken).Returns(streamOfStoredFiles); - _fileStore.GetFilePropertiesAsync(versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Version, versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Partition, null, DefaultCancellationToken).Returns(new FileProperties { ContentLength = streamOfStoredFiles.Length }); - - var retrieveResourceRequest = new RetrieveResourceRequest(_studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, framesToRequest, new[] { AcceptHeaderHelpers.CreateAcceptHeaderForGetFrame() }); - _dicomFrameHandler.GetFramesResourceAsync(streamOfStoredFiles, retrieveResourceRequest.Frames, true, "*").Throws(new FrameNotFoundException()); - - // Request for a specific frame on the instance. - await Assert.ThrowsAsync(() => _retrieveResourceService.GetInstanceResourceAsync( - retrieveResourceRequest, - DefaultCancellationToken)); - - streamOfStoredFiles.Dispose(); - } - - [Fact] - public async Task GivenStoredInstancesWithFrames_WhenRetrieveRequestForNonExistingFrame_ThenNotFoundIsThrown() - { - // Add multiple instances to validate that we evaluate the requested instance and ignore the other(s). - List versionedInstanceIdentifiers = SetupInstanceIdentifiersList(ResourceType.Frames); - var framesToRequest = new List { 1, 4 }; - - // For the instance, set up the fileStore to return a stream containing the file associated with the identifier with 3 frames. - Stream streamOfStoredFiles = (await RetrieveHelpers.StreamAndStoredFileFromDataset(RetrieveHelpers.GenerateDatasetsFromIdentifiers(versionedInstanceIdentifiers.First().VersionedInstanceIdentifier), _recyclableMemoryStreamManager, frames: 3)).Value; - _fileStore.GetFileAsync(versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Version, versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Partition, null, DefaultCancellationToken).Returns(streamOfStoredFiles); - _fileStore.GetFilePropertiesAsync(versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Version, versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Partition, null, DefaultCancellationToken).Returns(new FileProperties { ContentLength = streamOfStoredFiles.Length }); - - var retrieveResourceRequest = new RetrieveResourceRequest(_studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, framesToRequest, new[] { AcceptHeaderHelpers.CreateAcceptHeaderForGetFrame() }); - _dicomFrameHandler.GetFramesResourceAsync(streamOfStoredFiles, retrieveResourceRequest.Frames, true, "*").Throws(new FrameNotFoundException()); - - // Request 2 frames - one which exists and one which doesn't. - await Assert.ThrowsAsync(() => _retrieveResourceService.GetInstanceResourceAsync( - retrieveResourceRequest, - DefaultCancellationToken)); - - // Dispose the stream. - streamOfStoredFiles.Dispose(); - } - - [Fact] - public async Task GivenStoredInstancesWithFrames_WhenRetrieveRequestForFrames_ThenFramesInInstanceAreRetrievedSuccesfully() - { - // Add multiple instances to validate that we return the requested instance and ignore the other(s). - List versionedInstanceIdentifiers = SetupInstanceIdentifiersList(ResourceType.Frames); - var framesToRequest = new List { 1, 2 }; - - // For the first instance identifier, set up the fileStore to return a stream containing a file associated with the identifier. - KeyValuePair streamAndStoredFile = await RetrieveHelpers.StreamAndStoredFileFromDataset(RetrieveHelpers.GenerateDatasetsFromIdentifiers(versionedInstanceIdentifiers.First().VersionedInstanceIdentifier), _recyclableMemoryStreamManager, frames: 3); - _fileStore.GetFileAsync(versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Version, versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Partition, null, DefaultCancellationToken).Returns(streamAndStoredFile.Value); - _fileStore.GetFilePropertiesAsync(versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Version, versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Partition, null, DefaultCancellationToken).Returns(new FileProperties { ContentLength = streamAndStoredFile.Value.Length }); - - // Setup frame handler to return the frames as streams from the file. - Stream[] frames = framesToRequest.Select(f => GetFrameFromFile(streamAndStoredFile.Key.Dataset, f)).ToArray(); - var retrieveResourceRequest = new RetrieveResourceRequest(_studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, framesToRequest, new[] { AcceptHeaderHelpers.CreateAcceptHeaderForGetFrame() }); - _dicomFrameHandler.GetFramesResourceAsync(streamAndStoredFile.Value, retrieveResourceRequest.Frames, true, "*").Returns(frames); - _retrieveTranscoder.TranscodeFileAsync(streamAndStoredFile.Value, "*").Returns(streamAndStoredFile.Value); - - RetrieveResourceResponse response = await _retrieveResourceService.GetInstanceResourceAsync( - retrieveResourceRequest, - DefaultCancellationToken); - - IEnumerable streams = await response.GetStreamsAsync(); - // Validate response status code and ensure response streams has expected frames - it should be equivalent to what the store was set up to return. - AssertPixelDataEqual(DicomPixelData.Create(streamAndStoredFile.Key.Dataset).GetFrame(framesToRequest[0]), streams.ToList()[0]); - AssertPixelDataEqual(DicomPixelData.Create(streamAndStoredFile.Key.Dataset).GetFrame(framesToRequest[1]), streams.ToList()[1]); - - // Validate dicom request is populated with correct transcode values - ValidateDicomRequestIsPopulated(); - - streamAndStoredFile.Value.Dispose(); - - // Validate part count is equal to the number of frames returned - Assert.Equal(2, _dicomRequestContextAccessor.RequestContext.PartCount); - } - - [Theory] - [InlineData("*", "1.2.840.10008.1.2.1", "1.2.840.10008.1.2.1")] - [InlineData("1.2.840.10008.1.2.1", "1.2.840.10008.1.2.1", "1.2.840.10008.1.2.1")] - [InlineData("1.2.840.10008.1.2.4.90", "1.2.840.10008.1.2.1", "1.2.840.10008.1.2.4.90")] - public async Task GetInstances_WithAcceptType_ThenResponseContentTypeIsCorrect(string requestedTransferSyntax, string originalTransferSyntax, string expectedTransferSyntax) - { - // arrange object with originalTransferSyntax - List versionedInstanceIdentifiers = SetupInstanceIdentifiersList(ResourceType.Instance, instanceProperty: new InstanceProperties() { TransferSyntaxUid = originalTransferSyntax, FileProperties = null }); - - // For each instance identifier, set up the fileStore to return a stream containing a file associated with the identifier. - var streamsAndStoredFilesArray = await Task.WhenAll(versionedInstanceIdentifiers.Select( - x => RetrieveHelpers.StreamAndStoredFileFromDataset(RetrieveHelpers.GenerateDatasetsFromIdentifiers(x.VersionedInstanceIdentifier, originalTransferSyntax), _recyclableMemoryStreamManager))); - var streamsAndStoredFiles = new List>(streamsAndStoredFilesArray); - streamsAndStoredFiles.ForEach(x => _fileStore.GetFileAsync(versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Version, versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Partition, null, DefaultCancellationToken).Returns(x.Value)); - streamsAndStoredFiles.ForEach(x => _fileStore.GetStreamingFileAsync(versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Version, versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Partition, null, DefaultCancellationToken).Returns(x.Value)); - _fileStore.GetFilePropertiesAsync(versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Version, versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Partition, null, DefaultCancellationToken).Returns(new FileProperties { ContentLength = streamsAndStoredFiles.First().Value.Length }); - streamsAndStoredFiles.ForEach(x => _retrieveTranscoder.TranscodeFileAsync(x.Value, requestedTransferSyntax).Returns(CopyStream(x.Value))); - - // act - RetrieveResourceResponse response = await _retrieveResourceService.GetInstanceResourceAsync( - new RetrieveResourceRequest(_studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, new[] { AcceptHeaderHelpers.CreateAcceptHeaderForGetStudy(requestedTransferSyntax) }), - DefaultCancellationToken); - - // assert - await using IAsyncEnumerator enumerator = response.ResponseInstances.GetAsyncEnumerator(DefaultCancellationToken); - while (await enumerator.MoveNextAsync()) - { - Assert.Equal(expectedTransferSyntax, enumerator.Current.TransferSyntaxUid); - } - } - - [Theory] - [InlineData("*", "1.2.840.10008.1.2.1", "1.2.840.10008.1.2.1")] - [InlineData("1.2.840.10008.1.2.1", "1.2.840.10008.1.2.1", "1.2.840.10008.1.2.1")] - [InlineData("1.2.840.10008.1.2.1", "1.2.840.10008.1.2.4.57", "1.2.840.10008.1.2.1")] - public async Task GetFrames_WithAcceptType_ThenResponseContentTypeIsCorrect(string requestedTransferSyntax, string originalTransferSyntax, string expectedTransferSyntax) - { - // Add multiple instances to validate that we return the requested instance and ignore the other(s). - List versionedInstanceIdentifiers = SetupInstanceIdentifiersList(ResourceType.Frames, instanceProperty: new InstanceProperties() { TransferSyntaxUid = originalTransferSyntax }); - var framesToRequest = new List { 1, 2 }; - - // For the first instance identifier, set up the fileStore to return a stream containing a file associated with the identifier. - KeyValuePair streamAndStoredFile = await RetrieveHelpers.StreamAndStoredFileFromDataset(RetrieveHelpers.GenerateDatasetsFromIdentifiers(versionedInstanceIdentifiers.First().VersionedInstanceIdentifier, originalTransferSyntax), _recyclableMemoryStreamManager, frames: 3); - _fileStore.GetFileAsync(versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Version, versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Partition, null, DefaultCancellationToken).Returns(streamAndStoredFile.Value); - _fileStore.GetFilePropertiesAsync(versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Version, versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Partition, null, DefaultCancellationToken).Returns(new FileProperties { ContentLength = streamAndStoredFile.Value.Length }); - - // Setup frame handler to return the frames as streams from the file. - Stream[] frames = framesToRequest.Select(f => GetFrameFromFile(streamAndStoredFile.Key.Dataset, f)).ToArray(); - var retrieveResourceRequest = new RetrieveResourceRequest(_studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, framesToRequest, new[] { AcceptHeaderHelpers.CreateAcceptHeaderForGetFrame(requestedTransferSyntax) }); - _dicomFrameHandler.GetFramesResourceAsync(streamAndStoredFile.Value, retrieveResourceRequest.Frames, true, "*").Returns(frames); - - // act - RetrieveResourceResponse response = await _retrieveResourceService.GetInstanceResourceAsync( - retrieveResourceRequest, - DefaultCancellationToken); - // assert - await using IAsyncEnumerator enumerator = response.ResponseInstances.GetAsyncEnumerator(DefaultCancellationToken); - while (await enumerator.MoveNextAsync()) - { - Assert.Equal(expectedTransferSyntax, enumerator.Current.TransferSyntaxUid); - } - } - - [Fact] - public async Task GetFrames_WithSinglePartAcceptOnMultipleFrames_Throws() - { - // arrange - string requestedTransferSyntax = "*"; - - List versionedInstanceIdentifiers = SetupInstanceIdentifiersList(ResourceType.Frames); - var framesToRequest = new List { 1, 2 }; - var retrieveResourceRequest = new RetrieveResourceRequest(_studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, framesToRequest, new[] { AcceptHeaderHelpers.CreateAcceptHeaderForGetFrame(requestedTransferSyntax, KnownContentTypes.ApplicationOctetStream, null, PayloadTypes.SinglePart) }); - - // act and assert - await Assert.ThrowsAsync(() => - _retrieveResourceService.GetInstanceResourceAsync( - retrieveResourceRequest, - DefaultCancellationToken)); - } - - [Theory] - [InlineData("*", "1.2.840.10008.1.2.1", "*")] // this is the bug in old files, that is fixed for new files - [InlineData("1.2.840.10008.1.2.1", "1.2.840.10008.1.2.1", "1.2.840.10008.1.2.1")] - [InlineData("1.2.840.10008.1.2.4.90", "1.2.840.10008.1.2.1", "1.2.840.10008.1.2.4.90")] - public async Task GetInstances_WithAcceptTypeOnOldFile_ThenResponseContentTypeWithBackCompatWorks(string requestedTransferSyntax, string originalTransferSyntax, string expectedTransferSyntax) - { - // arrange object with originalTransferSyntax as null from DB to show backcompat - List versionedInstanceIdentifiers = SetupInstanceIdentifiersList(ResourceType.Instance); - - // For each instance identifier, set up the fileStore to return a stream containing a file associated with the identifier. - var streamsAndStoredFilesArray = await Task.WhenAll(versionedInstanceIdentifiers.Select( - x => RetrieveHelpers.StreamAndStoredFileFromDataset(RetrieveHelpers.GenerateDatasetsFromIdentifiers(x.VersionedInstanceIdentifier, originalTransferSyntax), _recyclableMemoryStreamManager))); - var streamsAndStoredFiles = new List>(streamsAndStoredFilesArray); - streamsAndStoredFiles.ForEach(x => _fileStore.GetFileAsync(versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Version, versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Partition, null, DefaultCancellationToken).Returns(x.Value)); - streamsAndStoredFiles.ForEach(x => _fileStore.GetStreamingFileAsync(versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Version, versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Partition, null, DefaultCancellationToken).Returns(x.Value)); - _fileStore.GetFilePropertiesAsync(versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Version, versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Partition, null, DefaultCancellationToken).Returns(new FileProperties { ContentLength = streamsAndStoredFiles.First().Value.Length }); - _fileStore.GetFilePropertiesAsync(versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Version, versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Partition, null, DefaultCancellationToken).Returns(new FileProperties { ContentLength = streamsAndStoredFiles.First().Value.Length }); - streamsAndStoredFiles.ForEach(x => _retrieveTranscoder.TranscodeFileAsync(x.Value, requestedTransferSyntax).Returns(CopyStream(x.Value))); - - // act - RetrieveResourceResponse response = await _retrieveResourceService.GetInstanceResourceAsync( - new RetrieveResourceRequest(_studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, new[] { AcceptHeaderHelpers.CreateAcceptHeaderForGetStudy(requestedTransferSyntax) }), - DefaultCancellationToken); - - // assert - await using IAsyncEnumerator enumerator = response.ResponseInstances.GetAsyncEnumerator(DefaultCancellationToken); - while (await enumerator.MoveNextAsync()) - { - Assert.Equal(expectedTransferSyntax, enumerator.Current.TransferSyntaxUid); - } - } - - [Fact] - public async Task GetStudy_WithMultipleInstanceAndTranscoding_ThrowsNotSupported() - { - // arrange object with originalTransferSyntax - List versionedInstanceIdentifiers = SetupInstanceIdentifiersList(ResourceType.Study); - - // act and assert - await Assert.ThrowsAsync(() => - _retrieveResourceService.GetInstanceResourceAsync( - new RetrieveResourceRequest(_studyInstanceUid, new[] { AcceptHeaderHelpers.CreateAcceptHeaderForGetStudy(transferSyntax: "1.2.840.10008.1.2.4.90") }), - DefaultCancellationToken)); - } - - [Fact] - public async Task GetFrames_WithLargeFileSize_ThrowsNotSupported() - { - // arrange - List versionedInstanceIdentifiers = SetupInstanceIdentifiersList(ResourceType.Frames); - var framesToRequest = new List { 1, 2 }; - // arrange fileSize to be greater than max supported - long aboveMaxFileSize = new RetrieveConfiguration().MaxDicomFileSize + 1; - _fileStore.GetFilePropertiesAsync(versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Version, versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Partition, null, DefaultCancellationToken).Returns(new FileProperties { ContentLength = aboveMaxFileSize }); - - // act and assert - await Assert.ThrowsAsync(() => - _retrieveResourceService.GetInstanceResourceAsync( - new RetrieveResourceRequest(_studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, framesToRequest, new[] { AcceptHeaderHelpers.CreateAcceptHeaderForGetFrame() }), - DefaultCancellationToken)); - - } - - [Fact] - public async Task GetFrames_WithNoTranscode_HitsCache() - { - // arrange - List versionedInstanceIdentifiers = SetupInstanceIdentifiersList(ResourceType.Frames, instanceProperty: new InstanceProperties() { HasFrameMetadata = true }); - var framesToRequest = new List { 1 }; - _fileStore.GetFilePropertiesAsync(versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Version, versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Partition, null, DefaultCancellationToken) - .Returns(new FileProperties { ContentLength = new RetrieveConfiguration().MaxDicomFileSize }); - - // act - await _retrieveResourceService.GetInstanceResourceAsync( - new RetrieveResourceRequest(_studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, framesToRequest, new[] { AcceptHeaderHelpers.CreateAcceptHeaderForGetFrame() }), - DefaultCancellationToken); - - // assert - var identifier = new InstanceIdentifier(_studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, Partition.Default); - await _instanceMetadataCache.Received(1).GetAsync(Arg.Any(), identifier, Arg.Any>>(), Arg.Any()); - await _framesRangeCache.Received(1).GetAsync(Arg.Any(), Arg.Any(), Arg.Any>>>(), Arg.Any()); - } - - [Fact] - public async Task GetFrames_WithNoTranscode_ReturnsFramesFromCurrentVersion() - { - // arrange - List versionedInstanceIdentifiers = SetupInstanceIdentifiersList( - ResourceType.Frames, - instanceProperty: new InstanceProperties() { HasFrameMetadata = true }); - var framesToRequest = new List { 1 }; - _fileStore.GetFilePropertiesAsync(versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Version, versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Partition, null, DefaultCancellationToken) - .Returns(new FileProperties { ContentLength = new RetrieveConfiguration().MaxDicomFileSize }); - - // act - await _retrieveResourceService.GetInstanceResourceAsync( - new RetrieveResourceRequest(_studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, framesToRequest, new[] { AcceptHeaderHelpers.CreateAcceptHeaderForGetFrame() }), - DefaultCancellationToken); - - // assert - var identifier = new InstanceIdentifier(_studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, Partition.Default); - await _instanceMetadataCache.Received(1).GetAsync(Arg.Any(), identifier, Arg.Any>>(), Arg.Any()); - await _framesRangeCache.Received(1).GetAsync( - Arg.Any(), - Arg.Is(x => x == 3), - Arg.Any>>>(), - Arg.Any()); - - await _fileStore.GetFileFrameAsync( - Arg.Is(x => x == 3), - Partition.Default, - Arg.Any(), - null, - Arg.Any()); - } - - [Fact] - public async Task GetFrames_WithUpdatedInstanceAndWithNoTranscode_ReturnsFramesFromOriginalVersion() - { - int originalVersion = 1; - // arrange - List versionedInstanceIdentifiers = SetupInstanceIdentifiersList( - ResourceType.Frames, - instanceProperty: new InstanceProperties() { HasFrameMetadata = true, OriginalVersion = originalVersion }); - var framesToRequest = new List { 1 }; - _fileStore.GetFilePropertiesAsync(originalVersion, versionedInstanceIdentifiers.First().VersionedInstanceIdentifier.Partition, null, DefaultCancellationToken) - .Returns(new FileProperties { ContentLength = new RetrieveConfiguration().MaxDicomFileSize }); - - // act - await _retrieveResourceService.GetInstanceResourceAsync( - new RetrieveResourceRequest(_studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, framesToRequest, new[] { AcceptHeaderHelpers.CreateAcceptHeaderForGetFrame() }), - DefaultCancellationToken); - - // assert - var identifier = new InstanceIdentifier(_studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, Partition.Default); - await _instanceMetadataCache.Received(1).GetAsync(Arg.Any(), identifier, Arg.Any>>(), Arg.Any()); - await _framesRangeCache.Received(1).GetAsync( - Arg.Any(), - Arg.Is(x => x == originalVersion), - Arg.Any>>>(), - Arg.Any()); - - await _fileStore.GetFileFrameAsync( - Arg.Is(x => x == originalVersion), - Partition.Default, - Arg.Any(), - null, - Arg.Any()); - } - - [Fact] - public async Task GetFramesWithFileProperties_WithNoTranscode_ExpectGetFileFrameAsyncUsedFileProperties() - { - // arrange - List versionedInstanceIdentifiers = SetupInstanceIdentifiersList( - ResourceType.Frames, - instanceProperty: new InstanceProperties() { HasFrameMetadata = true, FileProperties = DefaultFileProperties }); - InstanceMetadata instance = versionedInstanceIdentifiers[0]; - - var framesToRequest = new List { 1 }; - - _fileStore.GetFilePropertiesAsync( - instance.VersionedInstanceIdentifier.Version, - instance.VersionedInstanceIdentifier.Partition, - instance.InstanceProperties.FileProperties, - DefaultCancellationToken) - .Returns(new FileProperties { ContentLength = new RetrieveConfiguration().MaxDicomFileSize }); - - Dictionary range = new Dictionary(); - range.Add(0, new FrameRange(0, 1)); - - _framesRangeCache.GetAsync( - instance.VersionedInstanceIdentifier.Version, - instance.VersionedInstanceIdentifier.Version, - Arg.Any>>>(), - Arg.Any()).Returns(range); - - // act - RetrieveResourceResponse response = await _retrieveResourceService.GetInstanceResourceAsync( - new RetrieveResourceRequest(_studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, framesToRequest, new[] { AcceptHeaderHelpers.CreateAcceptHeaderForGetFrame() }), - DefaultCancellationToken); - - List fastFrames = await response.ResponseInstances.ToListAsync(); - Assert.NotEmpty(fastFrames); - - // assert - await _fileStore.GetFileFrameAsync( - Arg.Is(x => x == instance.VersionedInstanceIdentifier.Version), - Partition.Default, - Arg.Any(), - Arg.Is(f => f.Path == DefaultFileProperties.Path && f.ETag == DefaultFileProperties.ETag && f.ContentLength == DefaultFileProperties.ContentLength), - Arg.Any()); - } - - [Fact] - public async Task GetFramesWithFileProperties_WithUpdatedInstanceAndWithNoTranscode_ExpectGetFileFrameAsyncUsesFileProperties() - { - // arrange - List instances = SetupInstanceIdentifiersList( - ResourceType.Frames, - instanceProperty: new InstanceProperties() { HasFrameMetadata = true, OriginalVersion = 1, FileProperties = DefaultFileProperties }); - - var framesToRequest = new List { 1 }; - - var instance = instances[0]; - _fileStore.GetFilePropertiesAsync(instance.InstanceProperties.OriginalVersion.Value, instance.VersionedInstanceIdentifier.Partition, instance.InstanceProperties.FileProperties, DefaultCancellationToken) - .Returns(new FileProperties { ContentLength = new RetrieveConfiguration().MaxDicomFileSize }); - - Dictionary range = new Dictionary(); - range.Add(0, new FrameRange(0, 1)); - - _framesRangeCache.GetAsync( - instance.InstanceProperties.OriginalVersion, - instance.InstanceProperties.OriginalVersion.Value, - Arg.Any>>>(), - Arg.Any()).Returns(range); - - // act - RetrieveResourceResponse response = await _retrieveResourceService.GetInstanceResourceAsync( - new RetrieveResourceRequest(_studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, framesToRequest, new[] { AcceptHeaderHelpers.CreateAcceptHeaderForGetFrame() }), - DefaultCancellationToken); - - List fastFrames = await response.ResponseInstances.ToListAsync(); - Assert.NotEmpty(fastFrames); - - // assert - await _fileStore.GetFileFrameAsync( - Arg.Is(x => x == instance.InstanceProperties.OriginalVersion.Value), - Partition.Default, - Arg.Any(), - Arg.Is(f => f.Path == DefaultFileProperties.Path && f.ETag == DefaultFileProperties.ETag && f.ContentLength == DefaultFileProperties.ContentLength), - Arg.Any()); - } - - private List SetupInstanceIdentifiersList(ResourceType resourceType, Partition partition = null, InstanceProperties instanceProperty = null, bool withFileProperties = false, bool isOriginalVersion = false) - { - var dicomInstanceIdentifiersList = new List(); - - instanceProperty ??= withFileProperties ? new InstanceProperties { FileProperties = DefaultFileProperties } : new InstanceProperties(); - partition ??= _dicomRequestContextAccessor.RequestContext.DataPartition; - - switch (resourceType) - { - case ResourceType.Study: - dicomInstanceIdentifiersList.Add(new InstanceMetadata(new VersionedInstanceIdentifier(_studyInstanceUid, _firstSeriesInstanceUid, TestUidGenerator.Generate(), 0, partition), instanceProperty)); - dicomInstanceIdentifiersList.Add(new InstanceMetadata(new VersionedInstanceIdentifier(_studyInstanceUid, _firstSeriesInstanceUid, TestUidGenerator.Generate(), 1, partition), instanceProperty)); - dicomInstanceIdentifiersList.Add(new InstanceMetadata(new VersionedInstanceIdentifier(_studyInstanceUid, _secondSeriesInstanceUid, TestUidGenerator.Generate(), 2, partition), instanceProperty)); - _instanceStore.GetInstanceIdentifierWithPropertiesAsync(partition, _studyInstanceUid, null, null, isOriginalVersion, DefaultCancellationToken).Returns(dicomInstanceIdentifiersList); - break; - case ResourceType.Series: - dicomInstanceIdentifiersList.Add(new InstanceMetadata(new VersionedInstanceIdentifier(_studyInstanceUid, _firstSeriesInstanceUid, TestUidGenerator.Generate(), 0, partition), instanceProperty)); - dicomInstanceIdentifiersList.Add(new InstanceMetadata(new VersionedInstanceIdentifier(_studyInstanceUid, _firstSeriesInstanceUid, TestUidGenerator.Generate(), 1, partition), instanceProperty)); - _instanceStore.GetInstanceIdentifierWithPropertiesAsync(partition, _studyInstanceUid, _firstSeriesInstanceUid, null, isOriginalVersion, DefaultCancellationToken).Returns(dicomInstanceIdentifiersList); - break; - case ResourceType.Instance: - case ResourceType.Frames: - dicomInstanceIdentifiersList.Add(new InstanceMetadata(new VersionedInstanceIdentifier(_studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, 3, partition), instanceProperty)); - dicomInstanceIdentifiersList.Add(new InstanceMetadata(new VersionedInstanceIdentifier(_studyInstanceUid, _firstSeriesInstanceUid, TestUidGenerator.Generate(), 4, partition), instanceProperty)); - _instanceStore.GetInstanceIdentifierWithPropertiesAsync(Arg.Is(x => x.Key == partition.Key), _studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, isOriginalVersion, DefaultCancellationToken).Returns(dicomInstanceIdentifiersList.SkipLast(1).ToList()); - var identifier = new InstanceIdentifier(_studyInstanceUid, _firstSeriesInstanceUid, _sopInstanceUid, partition); - _instanceMetadataCache.GetAsync(Arg.Any(), identifier, Arg.Any>>(), Arg.Any()).Returns(dicomInstanceIdentifiersList.First()); - break; - } - return dicomInstanceIdentifiersList; - } - - private void ValidateDicomRequestIsPopulated(bool isTranscodeRequested = false, long sizeOfTranscode = 0) - { - Assert.Equal(isTranscodeRequested, _dicomRequestContextAccessor.RequestContext.IsTranscodeRequested); - Assert.Equal(sizeOfTranscode, _dicomRequestContextAccessor.RequestContext.BytesTranscoded); - } - - private void ValidateResponseStreams( - IEnumerable expectedFiles, - IEnumerable responseStreams) - { - var responseFiles = responseStreams.Select(x => DicomFile.Open(x)).ToList(); - - Assert.Equal(expectedFiles.Count(), responseFiles.Count); - - foreach (DicomFile expectedFile in expectedFiles) - { - DicomFile actualFile = responseFiles.First(x => x.Dataset.ToInstanceIdentifier(Partition.Default).Equals(expectedFile - .Dataset.ToInstanceIdentifier(Partition.Default))); - - // If the same transfer syntax as original, the files should be exactly the same - if (expectedFile.Dataset.InternalTransferSyntax == actualFile.Dataset.InternalTransferSyntax) - { - var expectedFileArray = FileToByteArray(expectedFile); - var actualFileArray = FileToByteArray(actualFile); - - Assert.Equal(expectedFileArray.Length, actualFileArray.Length); - - for (var ii = 0; ii < expectedFileArray.Length; ii++) - { - Assert.Equal(expectedFileArray[ii], actualFileArray[ii]); - } - } - else - { - throw new NotImplementedException("Transcoded files do not have an implemented validation mechanism."); - } - } - } - - private byte[] FileToByteArray(DicomFile file) - { - using MemoryStream memoryStream = _recyclableMemoryStreamManager.GetStream(); - file.Save(memoryStream); - return memoryStream.ToArray(); - } - - private Stream GetFrameFromFile(DicomDataset dataset, int frame) - { - IByteBuffer frameData = DicomPixelData.Create(dataset).GetFrame(frame); - return _recyclableMemoryStreamManager.GetStream("RetrieveResourceServiceTests.GetFrameFromFile", frameData.Data, 0, frameData.Data.Length); - } - - private Stream CopyStream(Stream source) - { - MemoryStream dest = _recyclableMemoryStreamManager.GetStream(); - source.CopyTo(dest); - dest.Seek(0, SeekOrigin.Begin); - return dest; - } - - private static void AssertPixelDataEqual(IByteBuffer expectedPixelData, Stream actualPixelData) - { - Assert.Equal(expectedPixelData.Size, actualPixelData.Length); - Assert.Equal(0, actualPixelData.Position); - for (var i = 0; i < expectedPixelData.Size; i++) - { - Assert.Equal(expectedPixelData.Data[i], actualPixelData.ReadByte()); - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Retrieve/TranscoderTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Retrieve/TranscoderTests.cs deleted file mode 100644 index 19b2713b8f..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Retrieve/TranscoderTests.cs +++ /dev/null @@ -1,347 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using EnsureThat; -using FellowOakDicom; -using FellowOakDicom.Imaging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Core.Features.Retrieve; -using Microsoft.Health.Dicom.Tests.Common; -using Microsoft.IO; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Retrieve; - -public class TranscoderTests -{ - private readonly ITranscoder _transcoder; - private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager; - - private static readonly HashSet SupportedTransferSyntaxesFor8BitTranscoding = new HashSet - { - DicomTransferSyntax.DeflatedExplicitVRLittleEndian, - DicomTransferSyntax.ExplicitVRBigEndian, - DicomTransferSyntax.ExplicitVRLittleEndian, - DicomTransferSyntax.ImplicitVRLittleEndian, - DicomTransferSyntax.JPEG2000Lossless, - DicomTransferSyntax.JPEG2000Lossy, - DicomTransferSyntax.JPEGProcess1, - DicomTransferSyntax.JPEGProcess2_4, - DicomTransferSyntax.RLELossless, - }; - - private static readonly HashSet SupportedTransferSyntaxesForOver8BitTranscoding = new HashSet - { - DicomTransferSyntax.DeflatedExplicitVRLittleEndian, - DicomTransferSyntax.ExplicitVRBigEndian, - DicomTransferSyntax.ExplicitVRLittleEndian, - DicomTransferSyntax.ImplicitVRLittleEndian, - DicomTransferSyntax.RLELossless, - }; - - private static readonly HashSet SupportedPhotometricInterpretations = new HashSet - { - PhotometricInterpretation.Monochrome1, - PhotometricInterpretation.Monochrome2, - PhotometricInterpretation.PaletteColor, - PhotometricInterpretation.Rgb, - PhotometricInterpretation.YbrFull, - PhotometricInterpretation.YbrFull422, - PhotometricInterpretation.YbrPartial422, - PhotometricInterpretation.YbrPartial420, - PhotometricInterpretation.YbrIct, - PhotometricInterpretation.YbrRct, - }; - - public TranscoderTests() - { - _recyclableMemoryStreamManager = new RecyclableMemoryStreamManager(); - _transcoder = new Transcoder(_recyclableMemoryStreamManager, NullLogger.Instance); - } - - [Theory] - [MemberData(nameof(Get16BitTranscoderCombos))] - public async Task GivenSupported16bitTransferSyntax_WhenRetrievingFileAndAskingForConversion_ReturnedFileHasExpectedTransferSyntax( - DicomTransferSyntax tsFrom, - DicomTransferSyntax tsTo, - PhotometricInterpretation photometricInterpretation) - { - EnsureArg.IsNotNull(photometricInterpretation, nameof(photometricInterpretation)); - EnsureArg.IsNotNull(tsFrom, nameof(tsFrom)); - EnsureArg.IsNotNull(tsTo, nameof(tsTo)); - (DicomFile dicomFile, Stream stream) = await StreamAndStoredFileFromDataset(photometricInterpretation, false, tsFrom); - dicomFile.Dataset.ToInstanceIdentifier(Partition.Default); - - Stream transcodedFile = await _transcoder.TranscodeFileAsync(stream, tsTo.UID.UID); - - ValidateTransferSyntax(tsTo, transcodedFile); - } - - [Theory] - [MemberData(nameof(Get16BitTranscoderCombos))] - public async Task GivenSupported16bitTransferSyntax_WhenRetrievingFrameAndAskingForConversion_ReturnedFileHasExpectedTransferSyntax( - DicomTransferSyntax tsFrom, - DicomTransferSyntax tsTo, - PhotometricInterpretation photometricInterpretation) - { - EnsureArg.IsNotNull(photometricInterpretation, nameof(photometricInterpretation)); - EnsureArg.IsNotNull(tsFrom, nameof(tsFrom)); - EnsureArg.IsNotNull(tsTo, nameof(tsTo)); - DicomFile dicomFile = (await StreamAndStoredFileFromDataset(photometricInterpretation, false, tsFrom)).dicomFile; - dicomFile.Dataset.ToInstanceIdentifier(Partition.Default); - - _transcoder.TranscodeFrame(dicomFile, 1, tsTo.UID.UID); - } - - public static IEnumerable GetSupported8BitTranscoderCombos() - { - HashSet fromList = SupportedTransferSyntaxesFor8BitTranscoding; - HashSet toList = SupportedTransferSyntaxesFor8BitTranscoding; - HashSet photometricInterpretations = SupportedPhotometricInterpretations; - - HashSet<(DicomTransferSyntax fromTs, DicomTransferSyntax toTs, PhotometricInterpretation photometricInterpretation)> supported8BitTranscoderCombos = - (from x in fromList from y in toList from z in photometricInterpretations select (x, y, z)).ToHashSet(); - - supported8BitTranscoderCombos.ExceptWith(GenerateUnsupported8BitFromJPEG2000GeneratorCombos()); - supported8BitTranscoderCombos.ExceptWith(GenerateUnsupported8BitFromJPEG2000TranscoderCombos()); - supported8BitTranscoderCombos.ExceptWith(GenerateUnsupported8BitFromJPEGProcessGeneratorCombos()); - supported8BitTranscoderCombos.ExceptWith(GenerateUnsupported8BitToJPEGProcessTranscoderCombos()); - supported8BitTranscoderCombos.ExceptWith(GenerateUnsupported8BitToJPEGTranscoderCombos()); - supported8BitTranscoderCombos.ExceptWith(GenerateUnsupported8BitFromJPEGProcessMonochromePITranscoderCombos()); - - return from x in supported8BitTranscoderCombos select new object[] { x.fromTs, x.toTs, x.photometricInterpretation }; - } - - public static IEnumerable GetUnsupported8BitGeneratorCombos() - { - HashSet<(DicomTransferSyntax fromTs, DicomTransferSyntax toTs, PhotometricInterpretation photometricInterpretation)> supported8BitTranscoderCombos = - new HashSet<(DicomTransferSyntax, DicomTransferSyntax, PhotometricInterpretation)>(); - - supported8BitTranscoderCombos.UnionWith(GenerateUnsupported8BitFromJPEG2000GeneratorCombos()); - supported8BitTranscoderCombos.UnionWith(GenerateUnsupported8BitFromJPEGProcessGeneratorCombos()); - - return from x in supported8BitTranscoderCombos select new object[] { x.fromTs, x.toTs, x.photometricInterpretation }; - } - - public static IEnumerable GetUnsupported8BitTranscoderCombos() - { - HashSet<(DicomTransferSyntax fromTs, DicomTransferSyntax toTs, PhotometricInterpretation photometricInterpretation)> supported8BitTranscoderCombos = - new HashSet<(DicomTransferSyntax, DicomTransferSyntax, PhotometricInterpretation)>(); - - supported8BitTranscoderCombos.UnionWith(GenerateUnsupported8BitFromJPEG2000TranscoderCombos()); - supported8BitTranscoderCombos.UnionWith(GenerateUnsupported8BitToJPEGProcessTranscoderCombos()); - supported8BitTranscoderCombos.UnionWith(GenerateUnsupported8BitFromJPEGProcessMonochromePITranscoderCombos()); - - return from x in supported8BitTranscoderCombos select new object[] { x.fromTs, x.toTs, x.photometricInterpretation }; - } - - public static IEnumerable Get16BitTranscoderCombos() - { - HashSet fromList = SupportedTransferSyntaxesForOver8BitTranscoding; - HashSet toList = SupportedTransferSyntaxesForOver8BitTranscoding; - HashSet photometricInterpretations = SupportedPhotometricInterpretations; - - return from x in fromList from y in toList from z in photometricInterpretations select new object[] { x, y, z }; - } - - public static HashSet<(DicomTransferSyntax, DicomTransferSyntax, PhotometricInterpretation)> GenerateUnsupported8BitFromJPEG2000GeneratorCombos() - { - HashSet fromTs = new HashSet - { - DicomTransferSyntax.JPEG2000Lossless, - DicomTransferSyntax.JPEG2000Lossy, - }; - HashSet toTs = new HashSet - { - DicomTransferSyntax.DeflatedExplicitVRLittleEndian, - DicomTransferSyntax.ExplicitVRBigEndian, - DicomTransferSyntax.ExplicitVRLittleEndian, - DicomTransferSyntax.ImplicitVRLittleEndian, - DicomTransferSyntax.JPEG2000Lossless, - DicomTransferSyntax.JPEG2000Lossy, - DicomTransferSyntax.JPEGProcess1, - DicomTransferSyntax.JPEGProcess2_4, - DicomTransferSyntax.RLELossless, - }; - HashSet photometricInterpretations = new HashSet - { - PhotometricInterpretation.Rgb, - PhotometricInterpretation.YbrFull422, - PhotometricInterpretation.YbrPartial422, - PhotometricInterpretation.YbrPartial420, - }; - - return (from x in fromTs from y in toTs from z in photometricInterpretations select (x, y, z)).ToHashSet(); - } - - public static HashSet<(DicomTransferSyntax, DicomTransferSyntax, PhotometricInterpretation)> GenerateUnsupported8BitFromJPEG2000TranscoderCombos() - { - HashSet fromTs = new HashSet - { - DicomTransferSyntax.JPEG2000Lossless, - DicomTransferSyntax.JPEG2000Lossy, - }; - HashSet toTs = new HashSet - { - DicomTransferSyntax.JPEG2000Lossless, - DicomTransferSyntax.JPEG2000Lossy, - }; - HashSet photometricInterpretations = new HashSet - { - PhotometricInterpretation.YbrIct, - PhotometricInterpretation.YbrRct, - }; - - return (from x in fromTs from y in toTs from z in photometricInterpretations select (x, y, z)).ToHashSet(); - } - - public static HashSet<(DicomTransferSyntax, DicomTransferSyntax, PhotometricInterpretation)> GenerateUnsupported8BitFromJPEGProcessGeneratorCombos() - { - HashSet fromTs = new HashSet - { - DicomTransferSyntax.JPEGProcess1, - DicomTransferSyntax.JPEGProcess2_4, - }; - HashSet toTs = new HashSet - { - DicomTransferSyntax.DeflatedExplicitVRLittleEndian, - DicomTransferSyntax.ExplicitVRBigEndian, - DicomTransferSyntax.ExplicitVRLittleEndian, - DicomTransferSyntax.ImplicitVRLittleEndian, - DicomTransferSyntax.JPEG2000Lossless, - DicomTransferSyntax.JPEG2000Lossy, - DicomTransferSyntax.JPEGProcess1, - DicomTransferSyntax.JPEGProcess2_4, - DicomTransferSyntax.RLELossless, - }; - HashSet photometricInterpretations = new HashSet - { - PhotometricInterpretation.Rgb, - PhotometricInterpretation.YbrFull, - PhotometricInterpretation.YbrFull422, - PhotometricInterpretation.YbrPartial422, - PhotometricInterpretation.YbrPartial420, - PhotometricInterpretation.YbrIct, - PhotometricInterpretation.YbrRct, - }; - - return (from x in fromTs from y in toTs from z in photometricInterpretations select (x, y, z)).ToHashSet(); - } - - public static HashSet<(DicomTransferSyntax, DicomTransferSyntax, PhotometricInterpretation)> GenerateUnsupported8BitToJPEGProcessTranscoderCombos() - { - HashSet fromTs = new HashSet - { - DicomTransferSyntax.DeflatedExplicitVRLittleEndian, - DicomTransferSyntax.ExplicitVRBigEndian, - DicomTransferSyntax.ExplicitVRLittleEndian, - DicomTransferSyntax.ImplicitVRLittleEndian, - DicomTransferSyntax.JPEG2000Lossless, - DicomTransferSyntax.JPEG2000Lossy, - DicomTransferSyntax.RLELossless, - }; - HashSet toTs = new HashSet - { - DicomTransferSyntax.JPEGProcess1, - DicomTransferSyntax.JPEGProcess2_4, - }; - HashSet photometricInterpretations = new HashSet - { - PhotometricInterpretation.Monochrome1, - PhotometricInterpretation.Monochrome2, - PhotometricInterpretation.YbrFull, - PhotometricInterpretation.YbrIct, - PhotometricInterpretation.YbrRct, - }; - - return (from x in fromTs from y in toTs from z in photometricInterpretations select (x, y, z)).ToHashSet(); - } - - public static HashSet<(DicomTransferSyntax, DicomTransferSyntax, PhotometricInterpretation)> GenerateUnsupported8BitToJPEGTranscoderCombos() - { - HashSet fromTs = new HashSet - { - DicomTransferSyntax.DeflatedExplicitVRLittleEndian, - DicomTransferSyntax.ExplicitVRBigEndian, - DicomTransferSyntax.ExplicitVRLittleEndian, - DicomTransferSyntax.ImplicitVRLittleEndian, - DicomTransferSyntax.RLELossless, - }; - HashSet toTs = new HashSet - { - DicomTransferSyntax.JPEG2000Lossless, - DicomTransferSyntax.JPEG2000Lossy, - DicomTransferSyntax.JPEGProcess1, - DicomTransferSyntax.JPEGProcess2_4, - }; - HashSet photometricInterpretations = new HashSet - { - PhotometricInterpretation.Rgb, - PhotometricInterpretation.YbrFull422, - PhotometricInterpretation.YbrPartial422, - PhotometricInterpretation.YbrPartial420, - }; - - return (from x in fromTs from y in toTs from z in photometricInterpretations select (x, y, z)).ToHashSet(); - } - - public static HashSet<(DicomTransferSyntax, DicomTransferSyntax, PhotometricInterpretation)> GenerateUnsupported8BitFromJPEGProcessMonochromePITranscoderCombos() - { - // bug in fo-dicom doesn't set up photometric interpretation correctly. - HashSet fromTs = new HashSet - { - DicomTransferSyntax.JPEGProcess1, - DicomTransferSyntax.JPEGProcess2_4, - }; - HashSet toTs = new HashSet - { - DicomTransferSyntax.DeflatedExplicitVRLittleEndian, - DicomTransferSyntax.ExplicitVRBigEndian, - DicomTransferSyntax.ExplicitVRLittleEndian, - DicomTransferSyntax.ImplicitVRLittleEndian, - DicomTransferSyntax.JPEG2000Lossless, - DicomTransferSyntax.JPEG2000Lossy, - DicomTransferSyntax.JPEGProcess1, - DicomTransferSyntax.JPEGProcess2_4, - DicomTransferSyntax.RLELossless, - }; - HashSet photometricInterpretations = new HashSet - { - PhotometricInterpretation.Monochrome1, - PhotometricInterpretation.Monochrome2, - PhotometricInterpretation.PaletteColor, - }; - - return (from x in fromTs from y in toTs from z in photometricInterpretations select (x, y, z)).ToHashSet(); - } - - private async Task<(DicomFile dicomFile, Stream stream)> StreamAndStoredFileFromDataset(PhotometricInterpretation photometricInterpretation, bool is8BitPixelData, DicomTransferSyntax transferSyntax) - { - var dicomFile = is8BitPixelData ? - Samples.CreateRandomDicomFileWith8BitPixelData(transferSyntax: transferSyntax.UID.UID, photometricInterpretation: photometricInterpretation.Value, frames: 2) - : Samples.CreateRandomDicomFileWith16BitPixelData(transferSyntax: transferSyntax.UID.UID, photometricInterpretation: photometricInterpretation.Value, frames: 2); - - MemoryStream stream = _recyclableMemoryStreamManager.GetStream(); - await dicomFile.SaveAsync(stream); - stream.Position = 0; - - return (dicomFile, stream); - } - - private static void ValidateTransferSyntax( - DicomTransferSyntax expectedTransferSyntax, - Stream responseStream) - { - DicomFile responseFile = DicomFile.Open(responseStream); - - Assert.Equal(expectedTransferSyntax, responseFile.Dataset.InternalTransferSyntax); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Security/PrincipalClaimsExtractorTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Security/PrincipalClaimsExtractorTests.cs deleted file mode 100644 index 07fc92ab94..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Security/PrincipalClaimsExtractorTests.cs +++ /dev/null @@ -1,148 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Security.Claims; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Features.Context; -using Microsoft.Health.Dicom.Core.Features.Security; -using NSubstitute; -using Xunit; -using Claim = System.Security.Claims.Claim; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Security; - -public class PrincipalClaimsExtractorTests -{ - private readonly IDicomRequestContextAccessor _dicomRequestContextAccessor = Substitute.For(); - private readonly IOptions _securityOptions = Substitute.For>(); - private readonly SecurityConfiguration _securityConfiguration = Substitute.For(); - private readonly ClaimsPrincipal _claimsPrincipal = Substitute.For(); - private readonly PrincipalClaimsExtractor _claimsIndexer; - - public PrincipalClaimsExtractorTests() - { - _securityOptions.Value.Returns(_securityConfiguration); - _dicomRequestContextAccessor.RequestContext.Principal.Returns(_claimsPrincipal); - _claimsIndexer = new PrincipalClaimsExtractor(_dicomRequestContextAccessor, _securityOptions); - } - - private static Claim Claim1 => new Claim("claim1", "value1"); - - private static Claim Claim2 => new Claim("claim2", "value2"); - - private static KeyValuePair ExpectedValue1 => new KeyValuePair("claim1", "value1"); - - private static KeyValuePair ExpectedValue2 => new KeyValuePair("claim1", "value1"); - - [Fact] - public void GivenANullDicomContextAccessor_WhenInitializing_ThenExceptionShouldBeThrown() - { - Assert.Throws( - "dicomRequestContextAccessor", - () => new PrincipalClaimsExtractor(null, Options.Create(new SecurityConfiguration()))); - } - - [Fact] - public void GivenANullSecurityConfiguration_WhenInitializing_ThenExceptionShouldBeThrown() - { - Assert.Throws( - "securityConfiguration", - () => new PrincipalClaimsExtractor(new DicomRequestContextAccessor(), null)); - } - - [Fact] - public void GivenANullPrincipal_WhenExtracting_ThenAnEmptyListShouldBeReturned() - { - _securityConfiguration.PrincipalClaims.Returns(new HashSet { "claim1" }); - var result = _claimsIndexer.Extract(); - - Assert.Empty(result); - } - - [Fact] - public void GivenAnEmptyListOfClaims_WhenExtracting_ThenAnEmptyListShouldBeReturned() - { - _securityConfiguration.PrincipalClaims.Returns(new HashSet { "claim1" }); - _claimsPrincipal.Claims.Returns(new List()); - - var result = _claimsIndexer.Extract(); - - Assert.Empty(result); - } - - [Fact] - public void GivenAnEmptyListOfLastModifiedClaims_WhenExtracting_ThenAnEmptyListShouldBeReturned() - { - _securityConfiguration.PrincipalClaims.Returns(new HashSet()); - _claimsPrincipal.Claims.Returns(new List { Claim1 }); - - var result = _claimsIndexer.Extract(); - - Assert.Empty(result); - } - - [Fact] - public void GivenAMismatchedListOfClaimsAndLastModifiedClaims_WhenExtracting_ThenAnEmptyListShouldBeReturned() - { - _securityConfiguration.PrincipalClaims.Returns(new HashSet { "claim2" }); - _claimsPrincipal.Claims.Returns(new List { Claim1 }); - - var result = _claimsIndexer.Extract(); - - Assert.Empty(result); - } - - [Fact] - public void GivenAMatchedListOfClaimsAndLastModifiedClaims_WhenExtracting_TheEntireSetShouldReturn() - { - _securityConfiguration.PrincipalClaims.Returns(new HashSet { "claim1" }); - _claimsPrincipal.Claims.Returns(new List { Claim1 }); - - var result = _claimsIndexer.Extract(); - - Assert.Contains(ExpectedValue1, result); - Assert.Single(result); - } - - [Fact] - public void GivenAMatchedListOfClaimsAndLastModifiedClaimsWithMultipleDifferentClaims_WhenExtracting_TheEntireSetShouldReturn() - { - _securityConfiguration.PrincipalClaims.Returns(new HashSet { "claim1", "claim2" }); - _claimsPrincipal.Claims.Returns(new List { Claim1, Claim2 }); - - var result = _claimsIndexer.Extract(); - - Assert.Contains(ExpectedValue1, result); - Assert.Contains(ExpectedValue2, result); - Assert.Equal(2, result.Count); - } - - [Fact] - public void GivenAMatchedListOfClaimsAndLastModifiedClaimsWithMultipleSimilar_WhenExtracting_TheEntireSetShouldReturn() - { - _securityConfiguration.PrincipalClaims.Returns(new HashSet { "claim1" }); - _claimsPrincipal.Claims.Returns(new List { Claim1, Claim1 }); - - var result = _claimsIndexer.Extract(); - - Assert.Contains(ExpectedValue1, result); - Assert.Equal(2, result.Count); - } - - [Fact] - public void GivenAPartiallyMatchedListOfClaimsAndLastModifiedClaims_WhenExtracting_ASubsetShouldReturn() - { - _securityConfiguration.PrincipalClaims.Returns(new HashSet { "claim1", "claim3" }); - _claimsPrincipal.Claims.Returns(new List { Claim1, Claim2 }); - - var result = _claimsIndexer.Extract(); - - Assert.Contains(ExpectedValue1, result); - Assert.Single(result); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Store/DicomStoreServiceTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Store/DicomStoreServiceTests.cs deleted file mode 100644 index 3127e55ca9..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Store/DicomStoreServiceTests.cs +++ /dev/null @@ -1,702 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using FellowOakDicom; -using Microsoft.ApplicationInsights.Channel; -using Microsoft.ApplicationInsights.Extensibility; -using Microsoft.ApplicationInsights; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Client; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Context; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Core.Features.Store; -using Microsoft.Health.Dicom.Core.Features.Store.Entries; -using Microsoft.Health.Dicom.Core.Features.Telemetry; -using Microsoft.Health.Dicom.Core.Features.Validation; -using Microsoft.Health.Dicom.Core.Messages.Store; -using Microsoft.Health.Dicom.Tests.Common; -using NSubstitute; -using NSubstitute.ExceptionExtensions; -using Xunit; -using DicomValidationException = FellowOakDicom.DicomValidationException; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Store; - -public class DicomStoreServiceTests -{ - private static readonly CancellationToken DefaultCancellationToken = new CancellationTokenSource().Token; - private static readonly StoreResponse DefaultResponse = new StoreResponse(StoreResponseStatus.Success, new DicomDataset(), null); - private static readonly StoreValidationResult DefaultStoreValidationResult = new StoreValidationResultBuilder().Build(); - private const string ExpectedAttributeError = - """DICOM100: (0008,0020) - Content "NotAValidStudyDate" does not validate VR DA: one of the date values does not match the pattern YYYYMMDD"""; - private const string ExpectedIssuerOfAccessionNumberSequenceError = - """DICOM100: (0008,0051) - Content "NotAValidStudyDate" does not validate VR DA: one of the date values does not match the pattern YYYYMMDD"""; - - private readonly DicomDataset _dicomDataset1 = Samples.CreateRandomInstanceDataset( - studyInstanceUid: "1", - seriesInstanceUid: "2", - sopInstanceUid: "3", - sopClassUid: "4"); - - private readonly DicomDataset _dicomDataset2 = Samples.CreateRandomInstanceDataset( - studyInstanceUid: "10", - seriesInstanceUid: "11", - sopInstanceUid: "12", - sopClassUid: "13"); - - private readonly IStoreResponseBuilder _storeResponseBuilder = Substitute.For(); - private readonly IStoreDatasetValidator _dicomDatasetValidator = Substitute.For(); - private readonly IStoreOrchestrator _storeOrchestrator = Substitute.For(); - private readonly IDicomRequestContextAccessor _dicomRequestContextAccessor = Substitute.For(); - private readonly IDicomRequestContextAccessor _dicomRequestContextAccessorLatestApi = Substitute.For(); - private readonly IDicomRequestContext _dicomRequestContext = Substitute.For(); - private readonly IDicomRequestContext _dicomRequestContextLatestApi = Substitute.For(); - private readonly StoreMeter _storeMeter = new StoreMeter(); - private readonly TelemetryClient _telemetryClient = new TelemetryClient(new TelemetryConfiguration() - { - TelemetryChannel = Substitute.For(), - }); - - private readonly IDicomTelemetryClient _dicomTelemetryClient = Substitute.For(); - private readonly StoreService _storeService; - private readonly StoreService _storeServiceDropData; - - public DicomStoreServiceTests() - { - _dicomRequestContext.DataPartition = Partition.Default; - _dicomRequestContextLatestApi.DataPartition = Partition.Default; - _storeResponseBuilder.BuildResponse(Arg.Any()).Returns(DefaultResponse); - _dicomRequestContextAccessor.RequestContext.Returns(_dicomRequestContext); - _dicomRequestContextAccessorLatestApi.RequestContext.Returns(_dicomRequestContextLatestApi); - _dicomRequestContextLatestApi.Version.Returns(Convert.ToInt32(DicomApiVersions.Latest.TrimStart('v'), CultureInfo - .CurrentCulture)); - - _dicomDatasetValidator - .ValidateAsync(Arg.Any(), Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(DefaultStoreValidationResult)); - - _storeService = new StoreService( - _storeResponseBuilder, - _dicomDatasetValidator, - _storeOrchestrator, - _dicomRequestContextAccessor, - _storeMeter, - NullLogger.Instance, - Options.Create(new FeatureConfiguration { }), - _dicomTelemetryClient, - _telemetryClient); - - _storeServiceDropData = new StoreService( - new StoreResponseBuilder(new MockUrlResolver(), Options.Create(new FeatureConfiguration { })), - CreateStoreDatasetValidatorWithDropDataEnabled(_dicomRequestContextAccessorLatestApi), - _storeOrchestrator, - _dicomRequestContextAccessorLatestApi, - _storeMeter, - NullLogger.Instance, - Options.Create(new FeatureConfiguration { }), - _dicomTelemetryClient, - _telemetryClient); - - DicomValidationBuilderExtension.SkipValidation(null); - } - - private static IStoreDatasetValidator CreateStoreDatasetValidatorWithDropDataEnabled(IDicomRequestContextAccessor contextAccessor) - { - IQueryTagService queryTagService = Substitute.For(); - List queryTags = new List(QueryTagService.CoreQueryTags); - queryTagService - .GetQueryTagsAsync(Arg.Any()) - .Returns(new List(QueryTagService.CoreQueryTags)); - StoreMeter storeMeter = new StoreMeter(); - - IStoreDatasetValidator validator = new StoreDatasetValidator( - Options.Create(new FeatureConfiguration() - { - EnableFullDicomItemValidation = true - }), - new ElementMinimumValidator(), - queryTagService, - storeMeter, - contextAccessor, - NullLogger.Instance); - return validator; - } - - [Fact] - public async Task GivenNullDicomInstanceEntries_WhenProcessed_ThenNoContentShouldBeReturned() - { - await ExecuteAndValidateAsync(dicomInstanceEntries: null); - - _storeResponseBuilder.DidNotReceiveWithAnyArgs().AddSuccess(default, DefaultStoreValidationResult, Partition.Default); - _storeResponseBuilder.DidNotReceiveWithAnyArgs().AddFailure(default); - } - - [Fact] - public async Task GivenEmptyDicomInstanceEntries_WhenProcessed_ThenNoContentShouldBeReturned() - { - await ExecuteAndValidateAsync(new IDicomInstanceEntry[0]); - - _storeResponseBuilder.DidNotReceiveWithAnyArgs().AddSuccess(default, DefaultStoreValidationResult, _dicomRequestContext.DataPartition); - _storeResponseBuilder.DidNotReceiveWithAnyArgs().AddFailure(default); - } - - [Fact] - public async Task GivenAValidDicomInstanceEntry_WhenProcessed_ThenSuccessfulEntryShouldBeAdded() - { - IDicomInstanceEntry dicomInstanceEntry = Substitute.For(); - - dicomInstanceEntry.GetDicomDatasetAsync(DefaultCancellationToken).Returns(_dicomDataset1); - long bytesStored = 100L; - _storeOrchestrator.StoreDicomInstanceEntryAsync(dicomInstanceEntry, DefaultCancellationToken).Returns(bytesStored); - - await ExecuteAndValidateAsync(dicomInstanceEntry); - - _storeResponseBuilder.Received(1).AddSuccess(_dicomDataset1, DefaultStoreValidationResult, _dicomRequestContext.DataPartition); - _storeResponseBuilder.DidNotReceiveWithAnyArgs().AddFailure(default); - Assert.Equal(1, _dicomRequestContextAccessor.RequestContext.PartCount); - Assert.Equal(bytesStored, _dicomRequestContextAccessor.RequestContext.TotalDicomEgressToStorageBytes); - } - - [Fact] - public async Task GiveAnInvalidDicomDataset_WhenProcessed_ThenFailedEntryShouldBeAddedWithProcessingFailure() - { - IDicomInstanceEntry dicomInstanceEntry = Substitute.For(); - - dicomInstanceEntry.GetDicomDatasetAsync(DefaultCancellationToken).Returns(_ => throw new Exception()); - - await ExecuteAndValidateAsync(dicomInstanceEntry); - - _storeResponseBuilder.DidNotReceiveWithAnyArgs().AddSuccess(default, DefaultStoreValidationResult, Partition.Default); - _storeResponseBuilder.Received(1).AddFailure(null, TestConstants.ProcessingFailureReasonCode); - Assert.Equal(0, _dicomRequestContextAccessor.RequestContext.TotalDicomEgressToStorageBytes); - await _storeOrchestrator.DidNotReceiveWithAnyArgs().StoreDicomInstanceEntryAsync(Arg.Any(), Arg.Any()); - } - - [Fact] - public async Task GivenADicomDatasetFailsToOpenDueToDicomValidationException_WhenProcessed_ThenFailedEntryShouldBeAddedWithValidationFailure() - { - IDicomInstanceEntry dicomInstanceEntry = Substitute.For(); - - dicomInstanceEntry.GetDicomDatasetAsync(DefaultCancellationToken).Returns(_ => throw new DicomValidationException("value", DicomVR.UI, string.Empty)); - - await ExecuteAndValidateAsync(dicomInstanceEntry); - - _storeResponseBuilder.DidNotReceiveWithAnyArgs().AddSuccess(default, DefaultStoreValidationResult, Partition.Default); - _storeResponseBuilder.Received(1).AddFailure(null, TestConstants.ValidationFailureReasonCode); - Assert.Equal(0, _dicomRequestContextAccessor.RequestContext.TotalDicomEgressToStorageBytes); - await _storeOrchestrator.DidNotReceiveWithAnyArgs().StoreDicomInstanceEntryAsync(Arg.Any(), Arg.Any()); - } - - [Fact] - public async Task GivenAValidationError_WhenDropDataEnabled_ThenSucceedsWithErrorsInFailedAttributesSequence() - { - // setup - IDicomInstanceEntry dicomInstanceEntry = Substitute.For(); - - DicomDataset dicomDataset = Samples.CreateRandomInstanceDataset(validateItems: false); - dicomDataset.Add(DicomTag.StudyDate, "NotAValidStudyDate"); - - dicomInstanceEntry.GetDicomDatasetAsync(DefaultCancellationToken).Returns(dicomDataset); - long bytesStored = 100L; - _storeOrchestrator.StoreDicomInstanceEntryAsync(dicomInstanceEntry, DefaultCancellationToken).Returns(bytesStored); - - // call - StoreResponse response = await _storeServiceDropData.ProcessAsync( - new[] { dicomInstanceEntry }, - null, - cancellationToken: DefaultCancellationToken); - - // assert response was successful - Assert.Equal(StoreResponseStatus.PartialSuccess, response.Status); - Assert.Null(response.Warning); - - // expect a single refSop sequence - DicomSequence refSopSequence = response.Dataset.GetSequence(DicomTag.ReferencedSOPSequence); - Assert.Single(refSopSequence); - - DicomDataset firstInstance = refSopSequence.Items[0]; - - // expect a comment sequence present - DicomSequence failedAttributesSequence = firstInstance.GetSequence(DicomTag.FailedAttributesSequence); - Assert.Single(failedAttributesSequence); - - // expect comment sequence has single warning about single invalid attribute - Assert.Equal( - ExpectedAttributeError, - failedAttributesSequence.Items[0].GetString(DicomTag.ErrorComment) - ); - - // expect that what we attempt to store has invalid attrs dropped - Assert.Throws(() => dicomDataset.GetString(DicomTag.StudyDate)); - - await _storeOrchestrator - .Received(1) - .StoreDicomInstanceEntryAsync( - dicomInstanceEntry, - DefaultCancellationToken - ); - - Assert.Equal(bytesStored, _dicomRequestContextAccessorLatestApi.RequestContext.TotalDicomEgressToStorageBytes); - } - - [Fact] - public async Task GivenAValidationErrorOnNonCoreTag_WhenDropDataEnabledAndFullDicomItemValidationEnabled_ThenSucceedsWithErrorsInFailedAttributesSequence() - { - // setup - IDicomInstanceEntry dicomInstanceEntry = Substitute.For(); - - DicomDataset dicomDataset = Samples.CreateRandomInstanceDataset(validateItems: false); - dicomDataset.Add(DicomTag.StudyDate, "NotAValidStudyDate"); - - dicomInstanceEntry.GetDicomDatasetAsync(DefaultCancellationToken).Returns(dicomDataset); - long bytesStored = 100L; - _storeOrchestrator.StoreDicomInstanceEntryAsync(dicomInstanceEntry, DefaultCancellationToken).Returns(bytesStored); - - // call - StoreResponse response = await _storeServiceDropData.ProcessAsync( - new[] { dicomInstanceEntry }, - null, - cancellationToken: DefaultCancellationToken); - - // assert response was successful - Assert.Equal(StoreResponseStatus.PartialSuccess, response.Status); - Assert.Null(response.Warning); - - // expect a single refSop sequence - DicomSequence refSopSequence = response.Dataset.GetSequence(DicomTag.ReferencedSOPSequence); - Assert.Single(refSopSequence); - - DicomDataset firstInstance = refSopSequence.Items[0]; - - // expect a comment sequence present - DicomSequence failedAttributesSequence = firstInstance.GetSequence(DicomTag.FailedAttributesSequence); - Assert.Single(failedAttributesSequence); - - // expect comment sequence has single warning about single invalid attribute - Assert.Equal( - ExpectedAttributeError, - failedAttributesSequence.Items[0].GetString(DicomTag.ErrorComment) - ); - - // expect that what we attempt to store has invalid attrs dropped - Assert.Throws(() => dicomDataset.GetString(DicomTag.StudyDate)); - - await _storeOrchestrator - .Received(1) - .StoreDicomInstanceEntryAsync( - dicomInstanceEntry, - DefaultCancellationToken - ); - - Assert.Equal(bytesStored, _dicomRequestContextAccessorLatestApi.RequestContext.TotalDicomEgressToStorageBytes); - } - - [Fact] - public async Task GivenASequenceWithOnlyInvalidAttributes_WhenDropDataEnabled_ThenSequenceDropped() - { - // setup - IDicomInstanceEntry dicomInstanceEntry = Substitute.For(); - DicomDataset dicomDataset = Samples.CreateRandomInstanceDataset(validateItems: false); - - dicomDataset.Add( - new DicomSequence( - DicomTag.IssuerOfAccessionNumberSequence, - new DicomDataset - { - { DicomTag.ReviewDate, "NotAValidReviewDate" }, - { DicomTag.StudyDate, "NotAValidStudyDate" } - }) - ); - - dicomInstanceEntry.GetDicomDatasetAsync(DefaultCancellationToken).Returns(dicomDataset); - - // call - StoreResponse response = await _storeServiceDropData.ProcessAsync( - new[] { dicomInstanceEntry }, - null, - cancellationToken: DefaultCancellationToken); - - // assert response was successful - Assert.Equal(StoreResponseStatus.PartialSuccess, response.Status); - Assert.Null(response.Warning); - - // expect a single refSop sequence - DicomSequence refSopSequence = response.Dataset.GetSequence(DicomTag.ReferencedSOPSequence); - Assert.Single(refSopSequence); - - DicomDataset firstInstance = refSopSequence.Items[0]; - - // expect a failed attr sequence present - DicomSequence failedAttributesSequence = firstInstance.GetSequence(DicomTag.FailedAttributesSequence); - - // even though the sequence has multiple attributes, we exist validation on the first error and only provide the - // first error - Assert.Single(failedAttributesSequence); - - // expect failed attr sequence has single warning about single invalid attribute, in this case the first we encounter - // which is last in the sequence - Assert.Equal( - """DICOM100: (0008,0051) - Content "NotAValidStudyDate" does not validate VR DA: one of the date values does not match the pattern YYYYMMDD""", - failedAttributesSequence.Items[0].GetString(DicomTag.ErrorComment) - ); - - // expect that what we attempt to store has invalid attrs dropped - Assert.Throws(() => dicomDataset.GetString(DicomTag.IssuerOfAccessionNumberSequence)); - - await _storeOrchestrator - .Received(1) - .StoreDicomInstanceEntryAsync( - dicomInstanceEntry, - DefaultCancellationToken - ); - } - - [Fact] - public async Task GivenASequenceWithOneValidAndOneInvalidAttribute_WhenDropDataEnabled_ThenSequenceDropped() - { - // setup - IDicomInstanceEntry dicomInstanceEntry = Substitute.For(); - DicomDataset dicomDataset = Samples.CreateRandomInstanceDataset(validateItems: false); - - dicomDataset.Add( - new DicomSequence( - DicomTag.IssuerOfAccessionNumberSequence, - new DicomDataset - { - { DicomTag.StudyDate, "NotAValidStudyDate" }, - { DicomTag.ReviewDate, "20220119" } - }) - ); - - dicomInstanceEntry.GetDicomDatasetAsync(DefaultCancellationToken).Returns(dicomDataset); - - // call - StoreResponse response = await _storeServiceDropData.ProcessAsync( - new[] { dicomInstanceEntry }, - null, - cancellationToken: DefaultCancellationToken); - - // assert response was successful - Assert.Equal(StoreResponseStatus.PartialSuccess, response.Status); - Assert.Null(response.Warning); - - // expect a single refSop sequence - DicomSequence refSopSequence = response.Dataset.GetSequence(DicomTag.ReferencedSOPSequence); - Assert.Single(refSopSequence); - - DicomDataset firstInstance = refSopSequence.Items[0]; - - // expect a failed attr sequence present - DicomSequence failedAttributesSequence = firstInstance.GetSequence(DicomTag.FailedAttributesSequence); - Assert.Single(failedAttributesSequence); - - // expect failed attr sequence has single warning about single invalid attribute - Assert.Equal( - ExpectedIssuerOfAccessionNumberSequenceError, - failedAttributesSequence.Items[0].GetString(DicomTag.ErrorComment) - ); - - // expect that what we attempt to store has invalid attrs dropped - Assert.Throws(() => dicomDataset.GetString(DicomTag.IssuerOfAccessionNumberSequence)); - - await _storeOrchestrator - .Received(1) - .StoreDicomInstanceEntryAsync( - dicomInstanceEntry, - DefaultCancellationToken - ); - } - - [Fact] - public async Task GivenMultipleInstancesAndOneHasInvalidAttr_WhenDropDataEnabled_ThenSucceedsWithErrorsInFailedAttributesSequenceForOneButNotTheOther() - { - // setup - IDicomInstanceEntry dicomInstanceEntryValid = Substitute.For(); - DicomDataset validDicomDataset = Samples.CreateRandomInstanceDataset(validateItems: false); - dicomInstanceEntryValid.GetDicomDatasetAsync(DefaultCancellationToken).Returns(validDicomDataset); - - IDicomInstanceEntry dicomInstanceEntryInvalid = Substitute.For(); - DicomDataset invalidDicomDataset = Samples.CreateRandomInstanceDataset(validateItems: false); - invalidDicomDataset.Add(DicomTag.StudyDate, "NotAValidStudyDate"); - dicomInstanceEntryInvalid.GetDicomDatasetAsync(DefaultCancellationToken).Returns(invalidDicomDataset); - - // call - StoreResponse response = await _storeServiceDropData.ProcessAsync( - new[] { dicomInstanceEntryValid, dicomInstanceEntryInvalid }, - null, - cancellationToken: DefaultCancellationToken); - - // assert response was successful - Assert.Equal(StoreResponseStatus.PartialSuccess, response.Status); - Assert.Null(response.Warning); - - // expect a two refSop sequences, one for each instance - DicomSequence refSopSequence = response.Dataset.GetSequence(DicomTag.ReferencedSOPSequence); - Assert.Equal(2, refSopSequence.Items.Count); - - // first was valid, expect a comment sequence present, but empty value - DicomDataset validInstanceResponse = refSopSequence.Items[0]; - Assert.Empty(validInstanceResponse.GetSequence(DicomTag.FailedAttributesSequence)); - - // second was invalid, expect a comment sequence present, and not empty value - DicomDataset invalidInstanceResponse = refSopSequence.Items[1]; - DicomSequence invalidFailedAttributesSequence = invalidInstanceResponse.GetSequence(DicomTag.FailedAttributesSequence); - // expect comment sequence has single warning about single invalid attribute - Assert.Equal( - ExpectedAttributeError, - invalidFailedAttributesSequence.Items[0].GetString(DicomTag.ErrorComment) - ); - - //expect that we stored both instances - await _storeOrchestrator - .Received() - .StoreDicomInstanceEntryAsync( - dicomInstanceEntryValid, - DefaultCancellationToken - ); - - // expect that what we attempt to store has invalid attrs dropped for invalid instance - Assert.Throws(() => invalidDicomDataset.GetString(DicomTag.StudyDate)); - await _storeOrchestrator - .Received() - .StoreDicomInstanceEntryAsync( - dicomInstanceEntryInvalid, - DefaultCancellationToken - ); - } - - [Fact] - public async Task GivenADicomInstanceAlreadyExistsExceptionWithConflictWhenStoring_WhenProcessed_ThenFailedEntryShouldBeAddedWithSopInstanceAlreadyExists() - { - IDicomInstanceEntry dicomInstanceEntry = Substitute.For(); - - dicomInstanceEntry.GetDicomDatasetAsync(DefaultCancellationToken).Returns(_dicomDataset2); - - _storeOrchestrator - .When(dicomStoreService => dicomStoreService.StoreDicomInstanceEntryAsync(dicomInstanceEntry, DefaultCancellationToken)) - .Do(_ => throw new InstanceAlreadyExistsException()); - - await ExecuteAndValidateAsync(dicomInstanceEntry); - - _storeResponseBuilder.DidNotReceiveWithAnyArgs().AddSuccess(default, DefaultStoreValidationResult, Partition.Default); - _storeResponseBuilder.Received(1).AddFailure(_dicomDataset2, TestConstants.SopInstanceAlreadyExistsReasonCode, null); - } - - [Fact] - public async Task GivenAnExceptionWhenStoring_WhenProcessed_ThenFailedEntryShouldBeAddedWithProcessingFailure() - { - IDicomInstanceEntry dicomInstanceEntry = Substitute.For(); - - dicomInstanceEntry.GetDicomDatasetAsync(DefaultCancellationToken).Returns(_dicomDataset2); - - _storeOrchestrator - .When(dicomStoreService => dicomStoreService.StoreDicomInstanceEntryAsync(dicomInstanceEntry, DefaultCancellationToken)) - .Do(_ => throw new DataStoreException("Simulated failure.")); - - await ExecuteAndValidateAsync(dicomInstanceEntry); - - _storeResponseBuilder.DidNotReceiveWithAnyArgs().AddSuccess(default, DefaultStoreValidationResult, Partition.Default); - _storeResponseBuilder.Received(1).AddFailure(_dicomDataset2, TestConstants.ProcessingFailureReasonCode); - } - - [Fact] - public async Task GivenAnExternalStoreExceptionWhenStoring_WhenProcessed_ThenExpectDataStoreExceptionRethrownAndResponseBuilderNotUsed() - { - IDicomInstanceEntry dicomInstanceEntry = Substitute.For(); - - dicomInstanceEntry.GetDicomDatasetAsync(DefaultCancellationToken).Returns(_dicomDataset2); - - _storeOrchestrator - .When(dicomStoreService => dicomStoreService.StoreDicomInstanceEntryAsync(dicomInstanceEntry, DefaultCancellationToken)) - .Do(_ => throw new DataStoreException("Simulated failure.", isExternal: true)); - - DataStoreException exception = await Assert.ThrowsAsync(() => _storeService.ProcessAsync(new[] { dicomInstanceEntry }, null, cancellationToken: DefaultCancellationToken)); - - Assert.True(exception.IsExternal); - - _storeResponseBuilder.DidNotReceiveWithAnyArgs().BuildResponse(Arg.Any(), Arg.Any()); - - _storeResponseBuilder.DidNotReceiveWithAnyArgs().AddSuccess(Arg.Any(), Arg.Any(), Arg.Any()); - _storeResponseBuilder.DidNotReceiveWithAnyArgs().AddFailure(Arg.Any(), Arg.Any()); - } - - [Fact] - public async Task GivenMultipleDicomInstanceEntriesWithFailure_WhenProcessed_ThenCorrespondingEntryShouldBeAdded() - { - IDicomInstanceEntry dicomInstanceEntryToSucceed = Substitute.For(); - IDicomInstanceEntry dicomInstanceEntryToFail = Substitute.For(); - long bytesStored = 100L; - _storeOrchestrator.StoreDicomInstanceEntryAsync(Arg.Any(), DefaultCancellationToken).Returns(bytesStored); - - dicomInstanceEntryToSucceed.GetDicomDatasetAsync(DefaultCancellationToken).Returns(_dicomDataset1); - dicomInstanceEntryToFail.GetDicomDatasetAsync(DefaultCancellationToken).Returns(_dicomDataset2); - - _dicomDatasetValidator - .When(datasetVaidator => datasetVaidator.ValidateAsync(_dicomDataset2, null, Arg.Any())) - .Do(_ => - { - throw new Exception(); - }); - - await ExecuteAndValidateAsync(dicomInstanceEntryToSucceed, dicomInstanceEntryToFail); - - _storeResponseBuilder.Received(1).AddSuccess(_dicomDataset1, DefaultStoreValidationResult, _dicomRequestContext.DataPartition); - _storeResponseBuilder.Received(1).AddFailure(_dicomDataset2, TestConstants.ProcessingFailureReasonCode); - Assert.Equal(2, _dicomRequestContextAccessor.RequestContext.PartCount); - - await _storeOrchestrator - .Received(1) - .StoreDicomInstanceEntryAsync( - dicomInstanceEntryToSucceed, - DefaultCancellationToken - ); - - await _storeOrchestrator - .DidNotReceive() - .StoreDicomInstanceEntryAsync( - dicomInstanceEntryToFail, - DefaultCancellationToken - ); - - Assert.Equal(bytesStored, _dicomRequestContextAccessor.RequestContext.TotalDicomEgressToStorageBytes); - } - - [Fact] - public async Task GivenMultipleDicomInstanceEntries_WhenProcessed_ThenTotalBytesTracked() - { - IDicomInstanceEntry dicomInstanceEntryToSucceed = Substitute.For(); - IDicomInstanceEntry dicomInstanceEntryToSucceed2 = Substitute.For(); - long bytesStored = 100L; - long totalExpectedBytesStored = bytesStored * 2; - _storeOrchestrator.StoreDicomInstanceEntryAsync(Arg.Any(), DefaultCancellationToken).Returns(bytesStored); - - dicomInstanceEntryToSucceed.GetDicomDatasetAsync(DefaultCancellationToken).Returns(_dicomDataset1); - dicomInstanceEntryToSucceed2.GetDicomDatasetAsync(DefaultCancellationToken).Returns(_dicomDataset2); - - await ExecuteAndValidateAsync(dicomInstanceEntryToSucceed, dicomInstanceEntryToSucceed2); - - _storeResponseBuilder.Received(1).AddSuccess(_dicomDataset1, DefaultStoreValidationResult, _dicomRequestContext.DataPartition); - _storeResponseBuilder.Received(1).AddSuccess(_dicomDataset2, DefaultStoreValidationResult, _dicomRequestContext.DataPartition); - - await _storeOrchestrator - .Received(1) - .StoreDicomInstanceEntryAsync( - dicomInstanceEntryToSucceed, - DefaultCancellationToken - ); - - await _storeOrchestrator - .Received(1) - .StoreDicomInstanceEntryAsync( - dicomInstanceEntryToSucceed2, - DefaultCancellationToken - ); - - Assert.Equal(totalExpectedBytesStored, _dicomRequestContextAccessor.RequestContext.TotalDicomEgressToStorageBytes); - } - - [Fact] - public async Task GivenRequiredStudyInstanceUid_WhenProcessed_ThenItShouldBePassed() - { - IDicomInstanceEntry dicomInstanceEntry = Substitute.For(); - - dicomInstanceEntry.GetDicomDatasetAsync(DefaultCancellationToken).Returns(_dicomDataset2); - - await ExecuteAndValidateAsync(dicomInstanceEntry); - } - - [Fact] - public async Task GivenFetchCancellation_WhenProcessed_ThenItShouldHaveThrown() - { - using CancellationTokenSource tokenSource = new CancellationTokenSource(); - IDicomInstanceEntry dicomInstanceEntry = Substitute.For(); - dicomInstanceEntry.GetDicomDatasetAsync(tokenSource.Token).Returns(ValueTask.FromException(new TaskCanceledException())); - - await Assert.ThrowsAsync(() => _storeService.ProcessAsync( - new IDicomInstanceEntry[] { dicomInstanceEntry }, - null, - cancellationToken: tokenSource.Token)); - } - - [Fact] - public async Task GivenValidationCancellation_WhenProcessed_ThenItShouldHaveThrown() - { - using CancellationTokenSource tokenSource = new CancellationTokenSource(); - IDicomInstanceEntry dicomInstanceEntry = Substitute.For(); - dicomInstanceEntry.GetDicomDatasetAsync(tokenSource.Token).Returns(_dicomDataset1); - _dicomDatasetValidator.ValidateAsync(_dicomDataset1, null, tokenSource.Token).ThrowsAsync(); - - await Assert.ThrowsAsync(() => _storeService.ProcessAsync( - new IDicomInstanceEntry[] { dicomInstanceEntry }, - null, - cancellationToken: tokenSource.Token)); - } - - [Fact] - public async Task GivenStowCancellation_WhenProcessed_ThenItShouldHaveThrown() - { - using CancellationTokenSource tokenSource = new CancellationTokenSource(); - IDicomInstanceEntry dicomInstanceEntry = Substitute.For(); - dicomInstanceEntry.GetDicomDatasetAsync(tokenSource.Token).Returns(_dicomDataset1); - _storeOrchestrator.StoreDicomInstanceEntryAsync(dicomInstanceEntry, tokenSource.Token).ThrowsAsync(new DataStoreException(new TaskCanceledException())); - - Exception actual = await Assert.ThrowsAsync(() => _storeService.ProcessAsync( - new IDicomInstanceEntry[] { dicomInstanceEntry }, - null, - cancellationToken: tokenSource.Token)); - Assert.IsType(actual.InnerException); - } - - private Task ExecuteAndValidateAsync(params IDicomInstanceEntry[] dicomInstanceEntries) - => ExecuteAndValidateAsync(requiredStudyInstanceUid: null, dicomInstanceEntries); - - private async Task ExecuteAndValidateAsync( - string requiredStudyInstanceUid, - params IDicomInstanceEntry[] dicomInstanceEntries) - { - StoreResponse response = await _storeService.ProcessAsync( - dicomInstanceEntries, - requiredStudyInstanceUid, - cancellationToken: DefaultCancellationToken); - - Assert.Equal(DefaultResponse, response); - - _storeResponseBuilder.Received(1).BuildResponse(requiredStudyInstanceUid); - - if (dicomInstanceEntries != null) - { - foreach (IDicomInstanceEntry dicomInstanceEntry in dicomInstanceEntries) - { - await ValidateDisposeAsync(dicomInstanceEntry); - } - } - } - - private static async Task ValidateDisposeAsync(IDicomInstanceEntry dicomInstanceEntry) - { - var timeout = DateTime.Now.AddSeconds(5); - - while (timeout < DateTime.Now) - { - if (dicomInstanceEntry.ReceivedCalls().Any()) - { - await dicomInstanceEntry.Received(1).DisposeAsync(); - break; - } - - await Task.Delay(100); - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Store/Entries/DicomInstanceEntryReaderForMultipartRequestTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Store/Entries/DicomInstanceEntryReaderForMultipartRequestTests.cs deleted file mode 100644 index fb1d4a62c5..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Store/Entries/DicomInstanceEntryReaderForMultipartRequestTests.cs +++ /dev/null @@ -1,145 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Health.Abstractions.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Store.Entries; -using Microsoft.Health.Dicom.Core.Web; -using NSubstitute; -using NSubstitute.Core; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Store.Entries; - -public class DicomInstanceEntryReaderForMultipartRequestTests -{ - private const string DefaultContentType = "multipart/related; boundary=123"; - private const string DefaultBodyPartContentType = "application/dicom"; - private static readonly CancellationToken DefaultCancellationToken = new CancellationTokenSource().Token; - - private readonly IMultipartReaderFactory _multipartReaderFactory = Substitute.For(); - private readonly DicomInstanceEntryReaderForMultipartRequest _dicomInstanceEntryReader; - - private readonly Stream _stream = new MemoryStream(); - - public DicomInstanceEntryReaderForMultipartRequestTests() - { - _dicomInstanceEntryReader = new DicomInstanceEntryReaderForMultipartRequest( - _multipartReaderFactory, - NullLogger.Instance); - } - - [Fact] - public void GivenAnInvalidContentType_WhenCanReadIsCalledForMultipartRequest_ThenFalseShouldBeReturned() - { - bool result = _dicomInstanceEntryReader.CanRead("dummy"); - - Assert.False(result); - } - - [Fact] - public void GivenAnNonMultipartRelatedContentType_WhenCanReadIsCalled_ThenFalseShouldBeReturned() - { - bool result = _dicomInstanceEntryReader.CanRead("multipart/data-form; boundary=123"); - - Assert.False(result); - } - - [Fact] - public void GivenAMultipartRelatedContentType_WhenCanReadIsCalled_ThenTrueShouldBeReturned() - { - bool result = _dicomInstanceEntryReader.CanRead(DefaultContentType); - - Assert.True(result); - } - - [Fact] - public Task GivenBodyPartWithInvalidContentType_WhenReading_ThenUnsupportedMediaTypeExceptionShouldBeThrown() - { - IMultipartReader multipartReader = SetupMultipartReader( - _ => new MultipartBodyPart("application/dicom+json", _stream), - _ => null); - - return Assert.ThrowsAsync(() => _dicomInstanceEntryReader.ReadAsync(DefaultContentType, _stream, DefaultCancellationToken)); - } - - [Fact] - public async Task GivenBodyPartWithValidContentType_WhenReading_ThenCorrectResultsShouldBeReturned() - { - IMultipartReader multipartReader = SetupMultipartReader( - _ => new MultipartBodyPart(DefaultBodyPartContentType, _stream), - _ => null); - - IReadOnlyCollection results = await _dicomInstanceEntryReader.ReadAsync( - DefaultContentType, - _stream, - DefaultCancellationToken); - - Assert.NotNull(results); - Assert.Collection( - results, - async item => - { - Assert.IsType(item); - Assert.Same(_stream, await item.GetStreamAsync(DefaultCancellationToken)); - }); - } - - [Fact] - public async Task GivenAnException_WhenReading_ThenAlreadyProcessedStreamsShouldBeDisposed() - { - IMultipartReader multipartReader = SetupMultipartReader( - _ => new MultipartBodyPart(DefaultBodyPartContentType, _stream), - _ => throw new Exception()); - - await Assert.ThrowsAsync(() => _dicomInstanceEntryReader.ReadAsync( - DefaultContentType, - _stream, - DefaultCancellationToken)); - - Assert.Throws(() => _stream.ReadByte()); - } - - [Fact] - public async Task GivenAnExceptionWhileDisposing_WhenReading_ThenItShouldNotInterfereWithDisposingOtherInstances() - { - var streamToBeDisposed = new MemoryStream(); - - IMultipartReader multipartReader = SetupMultipartReader( - _ => new MultipartBodyPart(DefaultBodyPartContentType, streamToBeDisposed), - _ => - { - // Dispose the previous stream so that when the code cleans up the resource, it throws exception. - streamToBeDisposed.Dispose(); - - return new MultipartBodyPart(DefaultBodyPartContentType, _stream); - }, - _ => throw new Exception()); - - await Assert.ThrowsAsync(() => _dicomInstanceEntryReader.ReadAsync( - DefaultContentType, - _stream, - DefaultCancellationToken)); - - Assert.Throws(() => _stream.ReadByte()); - } - - private IMultipartReader SetupMultipartReader(Func returnThis, params Func[] returnThese) - { - IMultipartReader multipartReader = Substitute.For(); - - multipartReader.ReadNextBodyPartAsync(DefaultCancellationToken).Returns(returnThis, returnThese); - - _multipartReaderFactory.Create(DefaultContentType, _stream) - .Returns(multipartReader); - - return multipartReader; - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Store/Entries/DicomInstanceEntryReaderForSinglePartRequestTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Store/Entries/DicomInstanceEntryReaderForSinglePartRequestTests.cs deleted file mode 100644 index ae2750be7a..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Store/Entries/DicomInstanceEntryReaderForSinglePartRequestTests.cs +++ /dev/null @@ -1,169 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Buffers; -using System.Collections.Generic; -using System.IO; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using Microsoft.Health.Abstractions.Exceptions; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Store.Entries; -using Microsoft.Health.Dicom.Core.Web; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Store.Entries; - -public class DicomInstanceEntryReaderForSinglePartRequestTests -{ - private const string DefaultContentType = "application/dicom"; - - private readonly ISeekableStreamConverter _seekableStreamConverter = new TestSeekableStreamConverter(); - private readonly DicomInstanceEntryReaderForSinglePartRequest _dicomInstanceEntryReader; - private readonly Stream _stream = new MemoryStream(); - - public DicomInstanceEntryReaderForSinglePartRequestTests() - { - _dicomInstanceEntryReader = new DicomInstanceEntryReaderForSinglePartRequest(_seekableStreamConverter, CreateStoreConfiguration(1000000), NullLogger.Instance); - } - - [Fact] - public void GivenAnInvalidContentType_WhenCanReadIsCalledForSinglePartRequest_ThenFalseShouldBeReturned() - { - bool result = _dicomInstanceEntryReader.CanRead("dummy"); - - Assert.False(result); - } - - [Fact] - public void GivenAnNonApplicationDicomContentType_WhenCanReadIsCalled_ThenFalseShouldBeReturned() - { - bool result = _dicomInstanceEntryReader.CanRead("multipart/related; boundary=123"); - - Assert.False(result); - } - - [Fact] - public void GivenAnApplicattionDicomContentType_WhenCanReadIsCalled_ThenTrueShouldBeReturned() - { - bool result = _dicomInstanceEntryReader.CanRead(DefaultContentType); - - Assert.True(result); - } - - [Fact] - public async Task GivenUnSupportedContentType_WhenReading_ThenShouldThrowUnsupportedMediaTypeExceptionAsync() - { - await Assert.ThrowsAsync(() => _dicomInstanceEntryReader.ReadAsync( - "not/application/dicom", - _stream, - CancellationToken.None)); - } - - [Fact] - public async Task GivenBodyPartWithValidContentType_WhenReading_ThenCorrectResultsShouldBeReturned() - { - using var source = new CancellationTokenSource(); - - _stream.Write(Encoding.UTF8.GetBytes("someteststring")); - - IReadOnlyCollection results = await _dicomInstanceEntryReader.ReadAsync( - DefaultContentType, - _stream, - source.Token); - - Assert.NotNull(results); - Assert.Collection( - results, - async item => - { - Assert.IsType(item); - Assert.Same(_stream, await item.GetStreamAsync(source.Token)); - }); - } - - [Fact] - public async Task GivenBodyPartWithValidContentTypeExceedLimit_ThrowError() - { - var dicomInstanceEntryReaderLowLimit = new DicomInstanceEntryReaderForSinglePartRequest(_seekableStreamConverter, CreateStoreConfiguration(1), NullLogger.Instance); - - using var source = new CancellationTokenSource(); - - Stream stream = new MemoryStream(); - stream.Write(Encoding.UTF8.GetBytes("someteststring")); - stream.Seek(0, SeekOrigin.Begin); - - await Assert.ThrowsAsync( - () => dicomInstanceEntryReaderLowLimit.ReadAsync( - DefaultContentType, - stream, - source.Token)); - } - - [Fact] - public async Task GivenBodyPartWithValidContentEqualsLimit_NoError() - { - var dicomInstanceEntryReaderLowLimit = new DicomInstanceEntryReaderForSinglePartRequest(_seekableStreamConverter, CreateStoreConfiguration(14), NullLogger.Instance); - - using var source = new CancellationTokenSource(); - - Stream stream = new MemoryStream(); - stream.Write(Encoding.UTF8.GetBytes("someteststring")); - stream.Seek(0, SeekOrigin.Begin); - - IReadOnlyCollection results = await _dicomInstanceEntryReader.ReadAsync( - DefaultContentType, - _stream, - source.Token); - - Assert.NotNull(results); - Assert.Collection( - results, - async item => - { - Assert.IsType(item); - Assert.Same(_stream, await item.GetStreamAsync(source.Token)); - }); - } - - private static IOptions CreateStoreConfiguration(long maxSize) - { - var configuration = Substitute.For>(); - configuration.Value.Returns(new StoreConfiguration - { - MaxAllowedDicomFileSize = maxSize, - }); - return configuration; - } - - private class TestSeekableStreamConverter : ISeekableStreamConverter - { - public async Task ConvertAsync(Stream stream, CancellationToken cancellationToken = default) - { - MemoryStream seekableStream = new MemoryStream(); - stream.CopyTo(seekableStream); - - await DrainAsync(seekableStream, cancellationToken); - - seekableStream.Seek(0, SeekOrigin.Begin); - - return seekableStream; - } - - private static async Task DrainAsync(Stream stream, CancellationToken cancellationToken = default) - { - const int bufferSize = 4096; - using IMemoryOwner bufferOwner = MemoryPool.Shared.Rent(bufferSize); - - while (await stream.ReadAsync(bufferOwner.Memory, cancellationToken) > 0) - { } - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Store/Entries/DicomInstanceEntryReaderManagerTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Store/Entries/DicomInstanceEntryReaderManagerTests.cs deleted file mode 100644 index ddd8e5f236..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Store/Entries/DicomInstanceEntryReaderManagerTests.cs +++ /dev/null @@ -1,43 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Microsoft.Health.Dicom.Core.Features.Store.Entries; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Store.Entries; - -public class DicomInstanceEntryReaderManagerTests -{ - private const string DefaultContentType = "test-content"; - - private readonly IDicomInstanceEntryReader _dicomInstanceEntryReader = Substitute.For(); - private readonly DicomInstanceEntryReaderManager _dicomInstanceEntryReaderManager; - - public DicomInstanceEntryReaderManagerTests() - { - _dicomInstanceEntryReader.CanRead(DefaultContentType).Returns(true); - - _dicomInstanceEntryReaderManager = new DicomInstanceEntryReaderManager( - new[] { _dicomInstanceEntryReader }); - } - - [Fact] - public void GivenASupportedContentType_WhenFindReaderIsCalled_ThenAnInstanceOfReaderShouldBeReturned() - { - Assert.Same( - _dicomInstanceEntryReader, - _dicomInstanceEntryReaderManager.FindReader(DefaultContentType)); - } - - [Theory] - [InlineData("unsupported")] - [InlineData("")] - [InlineData(null)] - public void GivenANotSupportedContentType_WhenFindReaderIsCalled_ThenNullShouldBeReturned(string contentType) - { - Assert.Null(_dicomInstanceEntryReaderManager.FindReader(contentType)); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Store/Entries/StreamOriginatedDicomInstanceEntryTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Store/Entries/StreamOriginatedDicomInstanceEntryTests.cs deleted file mode 100644 index 4bac047ddb..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Store/Entries/StreamOriginatedDicomInstanceEntryTests.cs +++ /dev/null @@ -1,76 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.IO; -using System.Threading.Tasks; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Store.Entries; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Store.Entries; - -public class StreamOriginatedDicomInstanceEntryTests -{ - private static readonly DicomDataset DefaultDicomDataset = new DicomDataset( - new DicomUniqueIdentifier(DicomTag.SOPClassUID, "123"), - new DicomUniqueIdentifier(DicomTag.SOPInstanceUID, "123")); - - [Fact] - public async Task GivenAnInvalidStream_WhenDicomDatasetIsRequested_ThenInvalidInstanceExceptionShouldBeThrown() - { - Stream stream = new MemoryStream(); - - StreamOriginatedDicomInstanceEntry dicomInstanceEntry = CreateStreamOriginatedDicomInstanceEntry(stream); - await Assert.ThrowsAsync(() => dicomInstanceEntry.GetDicomDatasetAsync(default).AsTask()); - } - - [Fact] - public async Task GivenAValidStream_WhenDicomDatasetIsRequested_ThenCorrectDatasetShouldBeReturned() - { - await using (Stream stream = await CreateStreamAsync(DefaultDicomDataset)) - { - StreamOriginatedDicomInstanceEntry dicomInstanceEntry = CreateStreamOriginatedDicomInstanceEntry(stream); - - DicomDataset dicomDataset = await dicomInstanceEntry.GetDicomDatasetAsync(default); - - Assert.NotNull(dicomDataset); - Assert.Equal(DefaultDicomDataset, dicomDataset); - } - } - - [Fact] - public async Task GivenAValidStream_WhenStreamIsRetrieved_ThenStreamShouldBeRewindToBeginning() - { - await using (Stream stream = await CreateStreamAsync(DefaultDicomDataset)) - { - // Force to move to the end of stream. - stream.Seek(0, SeekOrigin.End); - - StreamOriginatedDicomInstanceEntry dicomInstanceEntry = CreateStreamOriginatedDicomInstanceEntry(stream); - - Stream readStream = await dicomInstanceEntry.GetStreamAsync(default); - - Assert.NotNull(readStream); - Assert.Equal(0, readStream.Position); - } - } - - private static async Task CreateStreamAsync(DicomDataset dicomDataset) - { - var dicomFile = new DicomFile(dicomDataset); - - var stream = new MemoryStream(); - - await dicomFile.SaveAsync(stream); - - stream.Seek(0, SeekOrigin.Begin); - - return stream; - } - - private static StreamOriginatedDicomInstanceEntry CreateStreamOriginatedDicomInstanceEntry(Stream stream) - => new StreamOriginatedDicomInstanceEntry(stream); -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Store/StoreDatasetValidatorTestsV1.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Store/StoreDatasetValidatorTestsV1.cs deleted file mode 100644 index 2392cc72b6..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Store/StoreDatasetValidatorTestsV1.cs +++ /dev/null @@ -1,453 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using FellowOakDicom; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Features.Context; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Features.Store; -using Microsoft.Health.Dicom.Core.Features.Telemetry; -using Microsoft.Health.Dicom.Core.Features.Validation; -using Microsoft.Health.Dicom.Tests.Common; -using Microsoft.Health.Dicom.Tests.Common.Extensions; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Store; - -// Run these tests exclusively serial since they change the global autovalidation -[Collection("Auto-Validation Collection")] -public class StoreDatasetValidatorTestsV1 -{ - private const ushort ValidationFailedFailureCode = 43264; - private const ushort MismatchStudyInstanceUidFailureCode = 43265; - private IStoreDatasetValidator _dicomDatasetValidator; - private readonly DicomDataset _dicomDataset = Samples.CreateRandomInstanceDataset().NotValidated(); - private readonly IQueryTagService _queryTagService; - private readonly List _queryTags; - private readonly StoreMeter _storeMeter; - private readonly IDicomRequestContextAccessor _dicomRequestContextAccessor = Substitute.For(); - - public StoreDatasetValidatorTestsV1() - { - var featureConfiguration = Options.Create(new FeatureConfiguration() { EnableFullDicomItemValidation = false }); - var minValidator = new ElementMinimumValidator(); - _queryTagService = Substitute.For(); - _queryTags = new List(QueryTagService.CoreQueryTags); - _queryTagService.GetQueryTagsAsync(Arg.Any()).Returns(_queryTags); - _storeMeter = new StoreMeter(); - _dicomDatasetValidator = new StoreDatasetValidator(featureConfiguration, minValidator, _queryTagService, _storeMeter, _dicomRequestContextAccessor, NullLogger.Instance); - } - - [Fact] - public async Task GivenFullValidation_WhenPatientIDInvalid_ExpectErrorProduced() - { - var featureConfigurationEnableFullValidation = Substitute.For>(); - featureConfigurationEnableFullValidation.Value.Returns(new FeatureConfiguration - { - EnableFullDicomItemValidation = true, - }); - - DicomDataset dicomDataset = Samples.CreateRandomInstanceDataset( - validateItems: false, - patientId: "Before Null Character, \0"); - - IElementMinimumValidator minimumValidator = Substitute.For(); - - var dicomDatasetValidator = new StoreDatasetValidator( - featureConfigurationEnableFullValidation, - minimumValidator, - _queryTagService, - _storeMeter, - _dicomRequestContextAccessor, - NullLogger.Instance); - - var result = await dicomDatasetValidator.ValidateAsync( - dicomDataset, - null, - new CancellationToken()); - - Assert.Contains( - """does not validate VR LO: value contains invalid character""", - result.InvalidTagErrors[DicomTag.PatientID].Error); - minimumValidator.DidNotReceive().Validate(Arg.Any()); - } - - [Fact] - public async Task GivenPartialValidation_WhenPatientIDInvalid_ExpectTagValidatedAndErrorProduced() - { - - var featureConfigurationEnableFullValidation = Substitute.For>(); - featureConfigurationEnableFullValidation.Value.Returns(new FeatureConfiguration - { - EnableFullDicomItemValidation = false, - }); - - IElementMinimumValidator minimumValidator = new ElementMinimumValidator(); - - var dicomDatasetValidator = new StoreDatasetValidator( - featureConfigurationEnableFullValidation, - minimumValidator, - _queryTagService, - _storeMeter, - _dicomRequestContextAccessor, - NullLogger.Instance); - - DicomDataset dicomDataset = Samples.CreateRandomInstanceDataset( - validateItems: false, - patientId: "Before Null Character, \0"); - - var result = await dicomDatasetValidator.ValidateAsync( - dicomDataset, - null, - new CancellationToken()); - - Assert.True(result.InvalidTagErrors.Any()); - Assert.Single(result.InvalidTagErrors); - Assert.Equal( - """DICOM100: (0010,0020) - Dicom element 'PatientID' failed validation for VR 'LO': Value contains invalid character.""", - result.InvalidTagErrors[DicomTag.PatientID].Error); - } - - [Theory] - [InlineData("")] - [InlineData(null)] - public async Task GivenPatientIdEmpty_WhenValidated_ExpectErrorProduced(string value) - { - var featureConfigurationEnableFullValidation = Substitute.For>(); - featureConfigurationEnableFullValidation.Value.Returns(new FeatureConfiguration { }); - - DicomDataset dicomDataset = Samples.CreateRandomInstanceDataset( - validateItems: false, - patientId: value); - - if (value == null) - dicomDataset.AddOrUpdate(DicomTag.PatientID, new string[] { null }); - - IElementMinimumValidator minimumValidator = Substitute.For(); - - var dicomDatasetValidator = new StoreDatasetValidator( - featureConfigurationEnableFullValidation, - minimumValidator, - _queryTagService, - _storeMeter, - _dicomRequestContextAccessor, - NullLogger.Instance); - - var result = await dicomDatasetValidator.ValidateAsync( - dicomDataset, - null, - new CancellationToken()); - - Assert.Contains( - "DICOM100: (0010,0020) - The required tag '(0010,0020)' is missing.", - result.InvalidTagErrors[DicomTag.PatientID].Error); - } - - [Fact] - public async Task GivenPatientIdTagNotPresent_WhenValidated_ExpectErrorProduced() - { - var featureConfigurationEnableFullValidation = Substitute.For>(); - featureConfigurationEnableFullValidation.Value.Returns(new FeatureConfiguration { }); - - DicomDataset dicomDataset = Samples.CreateRandomInstanceDataset( - validateItems: false); - dicomDataset.Remove(DicomTag.PatientID); - - IElementMinimumValidator minimumValidator = Substitute.For(); - - var dicomDatasetValidator = new StoreDatasetValidator( - featureConfigurationEnableFullValidation, - minimumValidator, - _queryTagService, - _storeMeter, - _dicomRequestContextAccessor, - NullLogger.Instance); - - var result = await dicomDatasetValidator.ValidateAsync( - dicomDataset, - null, - new CancellationToken()); - - Assert.Contains( - "DICOM100: (0010,0020) - The required tag '(0010,0020)' is missing.", - result.InvalidTagErrors[DicomTag.PatientID].Error); - } - - [Fact] - public async Task GivenDicomTagWithDifferentVR_WhenValidated_ThenShouldReturnInvalidEntries() - { - var featureConfiguration = Options.Create(new FeatureConfiguration() { EnableFullDicomItemValidation = false }); - DicomTag tag = DicomTag.Date; - DicomElement element = new DicomDateTime(tag, DateTime.Now); - _dicomDataset.AddOrUpdate(element); - - _queryTags.Clear(); - _queryTags.Add(new QueryTag(tag.BuildExtendedQueryTagStoreEntry())); - IElementMinimumValidator validator = Substitute.For(); - _dicomDatasetValidator = new StoreDatasetValidator(featureConfiguration, validator, _queryTagService, _storeMeter, _dicomRequestContextAccessor, NullLogger.Instance); - - var result = await _dicomDatasetValidator.ValidateAsync(_dicomDataset, requiredStudyInstanceUid: null); - - Assert.True(result.InvalidTagErrors.Any()); - - validator.DidNotReceive().Validate(Arg.Any()); - } - - [Fact] - public async Task GivenAValidDicomDataset_WhenValidated_ThenItShouldSucceed() - { - var actual = await _dicomDatasetValidator.ValidateAsync(_dicomDataset, requiredStudyInstanceUid: null); - - Assert.Equal(ValidationWarnings.None, actual.WarningCodes); - } - - [Fact] - public async Task GivenDicomDatasetHavingDicomTagWithMultipleValues_WhenValidated_ThenItShouldReturnWarnings() - { - DicomElement element = new DicomLongString(DicomTag.StudyDescription, "Value1,", "Value2"); - var dicomDataset = Samples.CreateRandomInstanceDataset().NotValidated(); - dicomDataset.AddOrUpdate(element); - - var actual = await _dicomDatasetValidator.ValidateAsync(dicomDataset, requiredStudyInstanceUid: null); - - Assert.Equal(ValidationWarnings.IndexedDicomTagHasMultipleValues, actual.WarningCodes); - } - - [Theory] - [MemberData(nameof(GetNonExplicitVRTransferSyntax))] - public async Task GivenAValidDicomDatasetWithImplicitVR_WhenValidated_ReturnsExpectedWarning(DicomTransferSyntax transferSyntax) - { - var dicomDataset = Samples - .CreateRandomInstanceDataset(dicomTransferSyntax: transferSyntax) - .NotValidated(); - - var actual = await _dicomDatasetValidator.ValidateAsync(dicomDataset, requiredStudyInstanceUid: null); - - Assert.Equal(ValidationWarnings.DatasetDoesNotMatchSOPClass, actual.WarningCodes); - } - - [Fact] - public async Task GivenDicomDatasetWithImplicitVRAndHavingDicomTagWithMultipleValues_WhenValidated_ThenItShouldReturnWarnings() - { - DicomElement element = new DicomLongString(DicomTag.StudyDescription, "Value1,", "Value2"); - var dicomDataset = Samples.CreateRandomInstanceDataset(dicomTransferSyntax: DicomTransferSyntax.ImplicitVRBigEndian).NotValidated(); - dicomDataset.AddOrUpdate(element); - - var actual = await _dicomDatasetValidator.ValidateAsync(dicomDataset, requiredStudyInstanceUid: null); - - Assert.Equal(ValidationWarnings.IndexedDicomTagHasMultipleValues | ValidationWarnings.DatasetDoesNotMatchSOPClass, - actual.WarningCodes); - } - - [Fact] - public async Task GivenAValidDicomDatasetThatMatchesTheRequiredStudyInstanceUid_WhenValidated_ThenItShouldSucceed() - { - string studyInstanceUid = TestUidGenerator.Generate(); - - _dicomDataset.AddOrUpdate(DicomTag.StudyInstanceUID, studyInstanceUid); - - await _dicomDatasetValidator.ValidateAsync( - _dicomDataset, - studyInstanceUid); - } - - // Sometimes users will pass a whitespace padded UID. This is likely a misinterpretation of documentation - // specifying "If ending on an odd byte boundary, except when used for network negotiation (see PS3.8), - // one trailing NULL (00H), as a padding character, shall follow the last component in order to align the UID on an - // even byte boundary.": - // https://dicom.nema.org/dicom/2013/output/chtml/part05/chapter_9.html - [Theory] - [InlineData(" ", "")] - [InlineData(" ", "")] - [InlineData(" ", " ")] - [InlineData("", " ")] - public async Task GivenAValidDicomDatasetThatMatchesTheRequiredStudyInstanceUidWithUidWhitespacePadding_WhenValidated_ThenItShouldSucceed( - string queryStudyInstanceUidPadding, - string saveStudyInstanceUidPadding) - { - string studyInstanceUid = TestUidGenerator.Generate(); - string queryStudyInstanceUid = studyInstanceUid + queryStudyInstanceUidPadding; - string saveStudyInstanceUid = studyInstanceUid + saveStudyInstanceUidPadding; - - _dicomDataset.AddOrUpdate(DicomTag.StudyInstanceUID, saveStudyInstanceUid); - - - await _dicomDatasetValidator.ValidateAsync( - _dicomDataset, - queryStudyInstanceUid); - } - - public static IEnumerable GetDicomTagsToRemove() - { - return new List - { - new[] { DicomTag.PatientID.ToString() }, - new[] { DicomTag.StudyInstanceUID.ToString() }, - new[] { DicomTag.SeriesInstanceUID.ToString() }, - new[] { DicomTag.SOPInstanceUID.ToString() }, - new[] { DicomTag.SOPClassUID.ToString() }, - }; - } - - [Theory] - [MemberData(nameof(GetDicomTagsToRemove))] - public async Task GivenAMissingTag_WhenValidated_ThenDatasetValidationExceptionShouldBeThrown(string dicomTagInString) - { - DicomTag dicomTag = DicomTag.Parse(dicomTagInString); - - _dicomDataset.Remove(dicomTag); - - await ExecuteAndValidateInvalidTagEntries(dicomTag); - } - - public static IEnumerable GetDuplicatedDicomIdentifierValues() - { - return new List - { - new[] { DicomTag.StudyInstanceUID.ToString(), DicomTag.SeriesInstanceUID.ToString() }, - new[] { DicomTag.StudyInstanceUID.ToString(), DicomTag.SOPInstanceUID.ToString() }, - new[] { DicomTag.SeriesInstanceUID.ToString(), DicomTag.SOPInstanceUID.ToString() }, - }; - } - - [Fact] - public async Task GivenNonRequiredTagNull_ExpectTagValidatedAndNoErrorProduced() - { - DicomDataset dicomDataset = Samples.CreateRandomInstanceDataset(validateItems: false); - dicomDataset.Add(DicomTag.ContentDate, new string[] { null }); - dicomDataset.AddOrUpdate(DicomTag.PatientName, new string[] { null }); - dicomDataset.Add(DicomTag.WindowCenterWidthExplanation, new string[] { null }); - - var result = await _dicomDatasetValidator.ValidateAsync( - dicomDataset, - null, - new CancellationToken()); - - Assert.Empty(result.InvalidTagErrors); - } - - [Theory] - [MemberData(nameof(GetDuplicatedDicomIdentifierValues))] - public async Task GivenDuplicatedIdentifiers_WhenValidated_ThenValidationPasses(string firstDicomTagInString, string secondDicomTagInString) - { - DicomTag firstDicomTag = DicomTag.Parse(firstDicomTagInString); - DicomTag secondDicomTag = DicomTag.Parse(secondDicomTagInString); - - string value = _dicomDataset.GetSingleValue(firstDicomTag); - _dicomDataset.AddOrUpdate(secondDicomTag, value); - - var result = await _dicomDatasetValidator.ValidateAsync( - _dicomDataset, - null, - new CancellationToken()); - - Assert.Empty(result.InvalidTagErrors); - } - - [Fact] - public async Task GivenStudyInstanceUidDoesNotMatchWithRequiredStudyInstanceUid_WhenValidated_ThenDatasetValidationExceptionShouldBeThrown() - { - string requiredStudyInstanceUid = null; - string studyInstanceUid = _dicomDataset.GetSingleValue(DicomTag.StudyInstanceUID); - - do - { - requiredStudyInstanceUid = TestUidGenerator.Generate(); - } - while (string.Equals(requiredStudyInstanceUid, studyInstanceUid, StringComparison.InvariantCultureIgnoreCase)); - - await ExecuteAndValidateInvalidTagEntries(DicomTag.StudyInstanceUID, requiredStudyInstanceUid); - } - - [Fact] - public async Task GivenDatasetWithInvalidVrValue_WhenValidatingWithFullValidation_ThenDatasetValidationExceptionShouldBeThrown() - { - var featureConfiguration = Substitute.For>(); - featureConfiguration.Value.Returns(new FeatureConfiguration - { - EnableFullDicomItemValidation = true, - }); - var minValidator = new ElementMinimumValidator(); - - _dicomDatasetValidator = new StoreDatasetValidator(featureConfiguration, minValidator, _queryTagService, _storeMeter, _dicomRequestContextAccessor, NullLogger.Instance); - - // LO VR, invalid characters - _dicomDataset.Add(DicomTag.SeriesDescription, "CT1 abdomen\u0000"); - - await ExecuteAndValidateInvalidTagEntries(DicomTag.SeriesDescription); - } - - [Fact] - public async Task GivenDatasetWithInvalidIndexedTagValue_WhenValidating_ThenValidationExceptionShouldBeThrown() - { - // CS VR, > 16 characters is not allowed - _dicomDataset.Add(DicomTag.Modality, "01234567890123456789"); - - await ExecuteAndValidateInvalidTagEntries(DicomTag.Modality); - } - - [Fact] - public async Task GivenDatasetWithEmptyIndexedTagValue_WhenValidating_ThenValidationPasses() - { - _dicomDataset.AddOrUpdate(DicomTag.ReferringPhysicianName, string.Empty); - await _dicomDatasetValidator.ValidateAsync(_dicomDataset, null); - } - - [Fact] - public async Task GivenExtendedQueryTags_WhenValidating_ThenExtendedQueryTagsShouldBeValidated() - { - DicomTag standardTag = DicomTag.DestinationAE; - - // AE > 16 characters is not allowed - _dicomDataset.Add(standardTag, "01234567890123456"); - - QueryTag indextag = new QueryTag(standardTag.BuildExtendedQueryTagStoreEntry()); - _queryTags.Add(indextag); - await ExecuteAndValidateInvalidTagEntries(standardTag); - } - - [Fact] - public async Task GivenPrivateExtendedQueryTags_WhenValidating_ThenExtendedQueryTagsShouldBeValidated() - { - DicomTag tag = DicomTag.Parse("04050001"); - - DicomIntegerString element = new DicomIntegerString(tag, "0123456789123"); // exceed max length 12 - - // AE > 16 characters is not allowed - _dicomDataset.Add(element); - - QueryTag indextag = new QueryTag(tag.BuildExtendedQueryTagStoreEntry(vr: element.ValueRepresentation.Code)); - _queryTags.Clear(); - _queryTags.Add(indextag); - - await ExecuteAndValidateInvalidTagEntries(tag); - } - - private async Task ExecuteAndValidateInvalidTagEntries(DicomTag dicomTag, string requiredStudyInstanceUid = null) - { - var result = await _dicomDatasetValidator.ValidateAsync(_dicomDataset, requiredStudyInstanceUid); - - Assert.True(result.InvalidTagErrors.ContainsKey(dicomTag)); - } - - public static IEnumerable GetNonExplicitVRTransferSyntax() - { - foreach (var ts in Samples.GetAllDicomTransferSyntax()) - { - if (ts.IsExplicitVR) - continue; - - yield return new object[] { ts }; - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Store/StoreDatasetValidatorTestsV2.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Store/StoreDatasetValidatorTestsV2.cs deleted file mode 100644 index 2254f6ccb6..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Store/StoreDatasetValidatorTestsV2.cs +++ /dev/null @@ -1,409 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using FellowOakDicom; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Features.Context; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Features.Store; -using Microsoft.Health.Dicom.Core.Features.Telemetry; -using Microsoft.Health.Dicom.Core.Features.Validation; -using Microsoft.Health.Dicom.Tests.Common; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Store; - -// Run these tests exclusively serial since they change the global autovalidation -[Collection("Auto-Validation Collection")] -public class StoreDatasetValidatorTestsV2 -{ - private readonly IStoreDatasetValidator _dicomDatasetValidator; - private readonly DicomDataset _dicomDataset = Samples.CreateRandomInstanceDataset().NotValidated(); - private readonly IQueryTagService _queryTagService; - private readonly List _queryTags; - private readonly StoreMeter _storeMeter; - private readonly IDicomRequestContextAccessor _dicomRequestContextAccessorV2 = Substitute.For(); - private readonly IDicomRequestContext _dicomRequestContextV2 = Substitute.For(); - private readonly IElementMinimumValidator _minimumValidator = new ElementMinimumValidator(); - - public StoreDatasetValidatorTestsV2() - { - var featureConfiguration = Options.Create(new FeatureConfiguration() { EnableFullDicomItemValidation = false }); - _queryTagService = Substitute.For(); - _queryTags = new List(QueryTagService.CoreQueryTags); - _queryTagService.GetQueryTagsAsync(Arg.Any()).Returns(_queryTags); - _storeMeter = new StoreMeter(); - _dicomRequestContextV2.Version.Returns(2); - _dicomRequestContextAccessorV2.RequestContext.Returns(_dicomRequestContextV2); - _dicomDatasetValidator = new StoreDatasetValidator(featureConfiguration, _minimumValidator, _queryTagService, _storeMeter, _dicomRequestContextAccessorV2, NullLogger.Instance); - } - - [Fact(Skip = "Issue with minimum validation implementation, Fixing as part of https://github.com/microsoft/dicom-server/pull/3283")] - public async Task GivenFullValidation_WhenPatientIDInvalid_ExpectErrorProduced() - { - // Even when V2 api is requested, if full validation is enabled, we will validate and generate warnings on invalid tags - var featureConfigurationEnableFullValidation = Substitute.For>(); - featureConfigurationEnableFullValidation.Value.Returns(new FeatureConfiguration { EnableFullDicomItemValidation = true, }); - - DicomDataset dicomDataset = Samples.CreateRandomInstanceDataset( - validateItems: false, - patientId: "Before Null Character, \0"); - - var validator = new StoreDatasetValidator( - featureConfigurationEnableFullValidation, - _minimumValidator, - _queryTagService, - _storeMeter, - _dicomRequestContextAccessorV2, - NullLogger.Instance); - - var result = await validator.ValidateAsync( - dicomDataset, - null, - new CancellationToken()); - - Assert.Contains( - """does not validate VR LO: value contains invalid character""", - result.InvalidTagErrors[DicomTag.PatientID].Error); - - _minimumValidator.DidNotReceive().Validate(Arg.Any(), ValidationLevel.Default); - } - - [Fact] - public async Task GivenV2Enabled_WhenNonCoreTagInvalid_ExpectTagValidatedAndErrorProduced() - { - DicomDataset dicomDataset = Samples.CreateRandomInstanceDataset(validateItems: false); - dicomDataset.Add(DicomTag.ReviewDate, "NotAValidReviewDate"); - - var result = await _dicomDatasetValidator.ValidateAsync( - dicomDataset, - null, - new CancellationToken()); - - Assert.True(result.InvalidTagErrors.Any()); - Assert.Single(result.InvalidTagErrors); - Assert.Equal("""DICOM100: (300e,0004) - Content "NotAValidReviewDate" does not validate VR DA: one of the date values does not match the pattern YYYYMMDD""", result.InvalidTagErrors[DicomTag.ReviewDate].Error); - } - - [Fact] - public async Task GivenV2Enabled_WhenPrivateTagInvalid_ExpectTagValidatedAndWarningProduced() - { - DicomItem item = new DicomAgeString(new DicomTag(0007, 0008), "Invalid Private Age Tag"); - DicomDataset dicomDataset = Samples.CreateRandomInstanceDataset(validateItems: false); - dicomDataset.Add(item); - - var result = await _dicomDatasetValidator.ValidateAsync( - dicomDataset, - null); - - Assert.True(result.InvalidTagErrors.Any()); - Assert.Single(result.InvalidTagErrors); - Assert.Equal("""DICOM100: (0007,0008) - Content "Invalid Private Age Tag" does not validate VR AS: value does not have pattern 000[DWMY]""", result.InvalidTagErrors[item.Tag].Error); - } - - [Fact] - public async Task GivenV2Enabled_WhenItemNotADicomElement_ExpectTagValidationSkippedAndErrorNotProduced() - { - DicomDataset dicomDataset = Samples.CreateRandomInstanceDataset(validateItems: false); - dicomDataset.Add(new DicomOtherByteFragment(DicomTag.ReviewDate)); - - var result = await _dicomDatasetValidator.ValidateAsync( - dicomDataset, - null, - new CancellationToken()); - - Assert.Empty(result.InvalidTagErrors); - } - - [Fact] - public async Task GivenV2Enabled_WhenItemAnEmptyNotStringType_ExpectTagValidationNotSkippedAndErrorNotProduced() - { - DicomDataset dicomDataset = Samples.CreateRandomInstanceDataset(validateItems: false); - dicomDataset.Add(new DicomSignedLong(DicomTag.PregnancyStatus, new int[] { })); - - var result = await _dicomDatasetValidator.ValidateAsync( - dicomDataset, - null, - new CancellationToken()); - - Assert.Empty(result.InvalidTagErrors); - } - - [Theory] - [InlineData("X\0\0\0\0")] - [InlineData("\0")] - [InlineData("X")] - public async Task GivenV2Enabled_WhenNonCoreTagPaddedWithNNulls_ExpectTagValidatedAndNoErrorProduced(string value) - { - DicomDataset dicomDataset = Samples.CreateRandomInstanceDataset(validateItems: false); - dicomDataset.Add(DicomTag.Modality, value); - - var result = await _dicomDatasetValidator.ValidateAsync( - dicomDataset, - null, - new CancellationToken()); - - Assert.Empty(result.InvalidTagErrors); - } - - [Theory] - [InlineData("Before Null Character, \0\0\0\0")] - [InlineData("Before Null Character, \0")] - [InlineData("Before Null Character")] - public async Task GivenV2Enabled_WhenPatientIDPAddedWithNNulls_ExpectTagValidatedAndNoErrorProduced(string value) - { - DicomDataset dicomDataset = Samples.CreateRandomInstanceDataset( - validateItems: false, - patientId: value); - - var result = await _dicomDatasetValidator.ValidateAsync( - dicomDataset, - null, - new CancellationToken()); - - Assert.Empty(result.InvalidTagErrors); - } - - [Theory] - [InlineData("123,345")] - public async Task GivenV2Enabled_WhenPatientIDPAddedWithComma_ExpectTagValidatedAndNoErrorProduced(string value) - { - DicomDataset dicomDataset = Samples.CreateRandomInstanceDataset( - validateItems: false, - patientId: value); - - var result = await _dicomDatasetValidator.ValidateAsync( - dicomDataset, - null, - new CancellationToken()); - - Assert.Empty(result.InvalidTagErrors); - } - - [Theory] - [InlineData("")] - [InlineData(" ")] - [InlineData(null)] - public async Task GivenV2Enabled_WhenPatientIDTagPresentAndValueEmpty_ExpectTagValidatedAndWarningsProduced(string value) - { - DicomDataset dicomDataset = Samples.CreateRandomInstanceDataset( - validateItems: false, - patientId: value); - - if (value == null) - dicomDataset.AddOrUpdate(DicomTag.PatientID, new string[] { null }); - - var result = await _dicomDatasetValidator.ValidateAsync( - dicomDataset, - null, - new CancellationToken()); - - Assert.True(result.InvalidTagErrors.Any()); - Assert.Single(result.InvalidTagErrors); - Assert.False(result.HasCoreTagError); - Assert.False(result.InvalidTagErrors[DicomTag.PatientID].IsRequiredCoreTag); - Assert.Equal("DICOM100: (0010,0020) - The required tag '(0010,0020)' is missing.", result.InvalidTagErrors[DicomTag.PatientID].Error); - } - - [Fact] - public async Task GivenV2Enabled_WhenPatientIDTagNotPresent_ExpectErrorProduced() - { - DicomDataset dicomDataset = Samples.CreateRandomInstanceDataset( - validateItems: false); - dicomDataset.Remove(DicomTag.PatientID); - - var result = await _dicomDatasetValidator.ValidateAsync( - dicomDataset, - null, - new CancellationToken()); - - Assert.True(result.InvalidTagErrors.Any()); - Assert.Single(result.InvalidTagErrors); - Assert.True(result.HasCoreTagError); - Assert.True(result.InvalidTagErrors[DicomTag.PatientID].IsRequiredCoreTag); - Assert.Equal("DICOM100: (0010,0020) - The required tag '(0010,0020)' is missing.", result.InvalidTagErrors[DicomTag.PatientID].Error); - } - - [Fact] - public async Task GivenV2Enabled_WhenNonRequiredTagNull_ExpectTagValidatedAndNoErrorProduced() - { - DicomDataset dicomDataset = Samples.CreateRandomInstanceDataset(validateItems: false); - dicomDataset.Add(DicomTag.AcquisitionDateTime, new string[] { null }); - dicomDataset.AddOrUpdate(DicomTag.PatientName, new string[] { null }); - dicomDataset.Add(DicomTag.WindowCenterWidthExplanation, new string[] { null }); - - var result = await _dicomDatasetValidator.ValidateAsync( - dicomDataset, - null, - new CancellationToken()); - - Assert.Empty(result.InvalidTagErrors); - } - - [Fact(Skip = "Issue with minimum validation implementation, Fixing as part of https://github.com/microsoft/dicom-server/pull/3283")] - public async Task GivenV2Enabled_WhenCoreTagUidWithLeadingZeroes_ExpectTagValidatedAndNoOnlyWarningProduced() - { - // For Core Tag validation like studyInstanceUid, we expect to use minimum validator which is more lenient - // than fo-dicom's validator and allows things like leading zeroes in the UID - // We want the validation to *not* produce any errors and therefore not cause any failures - // However, we do want to still produce a warning for the end user so they are aware their instance may have issues - DicomDataset dicomDataset = Samples.CreateRandomInstanceDataset( - validateItems: false, - studyInstanceUid: "1.3.6.1.4.1.55648.014924617884283217793330176991551322645.2.1"); - - var result = await _dicomDatasetValidator.ValidateAsync( - dicomDataset, - null, - new CancellationToken()); - - Assert.Single(result.InvalidTagErrors); - Assert.False(result.InvalidTagErrors.Values.First().IsRequiredCoreTag); // we only fail when invalid core tags are present - Assert.Contains("does not validate VR UI: components must not have leading zeros", result.InvalidTagErrors.Values.First().Error); - } - - [Fact] - public async Task GivenV2Enabled_WhenValidSequenceTag_ExpectTagValidatedAndNoErrorProduced() - { - DicomDataset dicomDataset = Samples.CreateRandomInstanceDataset(validateItems: false); - dicomDataset.Add(new DicomSequence( - DicomTag.RegistrationSequence, - new DicomDataset - { - { DicomTag.PatientName, "Test^Patient" }, - { DicomTag.PixelData, new byte[] { 1, 2, 3 } }, - {DicomTag.AcquisitionDateTime, new string[] { null }} - })); - - var result = await _dicomDatasetValidator.ValidateAsync( - dicomDataset, - null, - new CancellationToken()); - - Assert.Empty(result.InvalidTagErrors); - } - - [Fact] - public async Task GivenV2Enabled_WhenValidSequenceTagWithInnerSequences_ExpectTagValidatedAndNoErrorProduced() - { - DicomDataset dicomDataset = Samples.CreateRandomInstanceDataset(validateItems: false); - dicomDataset.Add( - new DicomSequence( - DicomTag.RegistrationSequence, - new DicomDataset - { - { DicomTag.PatientName, "Test^Patient" }, - new DicomSequence( - DicomTag.RegistrationSequence, - new DicomDataset - { - { DicomTag.PatientName, "Test^Patient" }, - new DicomSequence( - DicomTag.RegistrationSequence, - new DicomDataset - { - { DicomTag.PatientName, "Test^Patient" }, - }) - }) - })); - - var result = await _dicomDatasetValidator.ValidateAsync( - dicomDataset, - null, - new CancellationToken()); - - Assert.Empty(result.InvalidTagErrors); - } - - [Fact] - public async Task GivenV2Enabled_WhenValidSequenceTagInvalidInnerNullPaddedValues_ExpectTagValidatedAndNoErrorProduced() - { - DicomDataset dicomDataset = Samples.CreateRandomInstanceDataset(validateItems: false); - var sq = new DicomDataset(); - sq.NotValidated(); - sq.AddOrUpdate(DicomTag.ReviewDate, "NotAValidReviewDate"); - - dicomDataset.Add(new DicomSequence(DicomTag.RegistrationSequence, sq)); - - - var result = await _dicomDatasetValidator.ValidateAsync( - dicomDataset, - null, - new CancellationToken()); - - Assert.Single(result.InvalidTagErrors); - Assert.Equal("""DICOM100: (300e,0004) - Content "NotAValidReviewDate" does not validate VR DA: one of the date values does not match the pattern YYYYMMDD""", result.InvalidTagErrors[DicomTag.ReviewDate].Error); - } - - [Fact] - public async Task GivenV2Enabled_WhenValidSequenceTagWithInvalidNestedValue_ExpectTagValidatedAndErrorProduced() - { - DicomDataset dicomDataset = Samples.CreateRandomInstanceDataset(validateItems: false); - - var sq = new DicomDataset(); - sq.NotValidated(); - sq.AddOrUpdate(DicomTag.ReviewDate, "NotAValidReviewDate"); - - var ds = new DicomDataset(); - ds.NotValidated(); - ds.Add(DicomTag.PatientName, "Test^Patient"); - ds.Add(DicomTag.RegistrationSequence, sq); - - dicomDataset.Add(new DicomSequence( - DicomTag.RegistrationSequence, - ds)); - - var result = await _dicomDatasetValidator.ValidateAsync( - dicomDataset, - null, - new CancellationToken()); - - Assert.Single(result.InvalidTagErrors); - Assert.Equal("""DICOM100: (300e,0004) - Content "NotAValidReviewDate" does not validate VR DA: one of the date values does not match the pattern YYYYMMDD""", result.InvalidTagErrors[DicomTag.ReviewDate].Error); - } - - [Fact] - public async Task GivenAValidDicomDataset_WhenValidated_ThenItShouldSucceed() - { - var result = await _dicomDatasetValidator.ValidateAsync(_dicomDataset, requiredStudyInstanceUid: null); - - Assert.Empty(result.InvalidTagErrors); - Assert.Equal(ValidationWarnings.None, result.WarningCodes); - } - - [Theory] - [MemberData(nameof(GetDuplicatedDicomIdentifierValues))] - public async Task GivenDuplicatedIdentifiers_WhenValidated_ThenValidationPasses(string firstDicomTagInString, string secondDicomTagInString) - { - DicomTag firstDicomTag = DicomTag.Parse(firstDicomTagInString); - DicomTag secondDicomTag = DicomTag.Parse(secondDicomTagInString); - - string value = _dicomDataset.GetSingleValue(firstDicomTag); - _dicomDataset.AddOrUpdate(secondDicomTag, value); - - var result = await _dicomDatasetValidator.ValidateAsync( - _dicomDataset, - null, - new CancellationToken()); - - Assert.Empty(result.InvalidTagErrors); - } - - public static IEnumerable GetDuplicatedDicomIdentifierValues() - { - return new List - { - new[] { DicomTag.StudyInstanceUID.ToString(), DicomTag.SeriesInstanceUID.ToString() }, - new[] { DicomTag.StudyInstanceUID.ToString(), DicomTag.SOPInstanceUID.ToString() }, - new[] { DicomTag.SeriesInstanceUID.ToString(), DicomTag.SOPInstanceUID.ToString() }, - }; - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Store/StoreHandlerTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Store/StoreHandlerTests.cs deleted file mode 100644 index 0b1ae7034d..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Store/StoreHandlerTests.cs +++ /dev/null @@ -1,85 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using FellowOakDicom; -using Microsoft.Health.Abstractions.Exceptions; -using Microsoft.Health.Core.Features.Security.Authorization; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Security; -using Microsoft.Health.Dicom.Core.Features.Store; -using Microsoft.Health.Dicom.Core.Features.Store.Entries; -using Microsoft.Health.Dicom.Core.Messages.Store; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Store; - -public class StoreHandlerTests -{ - private const string DefaultContentType = "application/dicom"; - - private readonly IDicomInstanceEntryReaderManager _dicomInstanceEntryReaderManager = Substitute.For(); - private readonly IStoreService _storeService = Substitute.For(); - private readonly StoreHandler _storeHandler; - - public StoreHandlerTests() - { - _storeHandler = new StoreHandler(new DisabledAuthorizationService(), _dicomInstanceEntryReaderManager, _storeService); - } - - [Fact] - public async Task GivenNullRequestBody_WhenHandled_ThenBadRequestExceptionShouldBeThrown() - { - var storeRequest = new StoreRequest(null, DefaultContentType); - - await Assert.ThrowsAsync(() => _storeHandler.Handle(storeRequest, CancellationToken.None)); - } - - [Fact] - public async Task GivenInvalidStudyInstanceUid_WhenHandled_ThenInvalidIdentifierExceptionShouldBeThrown() - { - var storeRequest = new StoreRequest(Stream.Null, DefaultContentType, "invalid"); - - await Assert.ThrowsAsync(() => _storeHandler.Handle(storeRequest, CancellationToken.None)); - } - - [Theory] - [InlineData("invalid")] - [InlineData("")] - [InlineData(null)] - public async Task GivenUnsupportedContentType_WhenHandled_ThenUnsupportedMediaTypeExceptionShouldBeThrown(string requestContentType) - { - _dicomInstanceEntryReaderManager.FindReader(default).ReturnsForAnyArgs((IDicomInstanceEntryReader)null); - - var storeRequest = new StoreRequest(Stream.Null, requestContentType); - - await Assert.ThrowsAsync(() => _storeHandler.Handle(storeRequest, CancellationToken.None)); - } - - [Fact] - public async Task GivenSupportedContentType_WhenHandled_ThenCorrectStoreResponseShouldBeReturned() - { - const string studyInstanceUid = "1.2.3"; - - IDicomInstanceEntry[] dicomInstanceEntries = Array.Empty(); - IDicomInstanceEntryReader dicomInstanceEntryReader = Substitute.For(); - var storeResponse = new StoreResponse(StoreResponseStatus.Success, new DicomDataset(), null); - using var source = new CancellationTokenSource(); - - dicomInstanceEntryReader.ReadAsync(DefaultContentType, Stream.Null, source.Token).Returns(dicomInstanceEntries); - _dicomInstanceEntryReaderManager.FindReader(DefaultContentType).Returns(dicomInstanceEntryReader); - _storeService.ProcessAsync(dicomInstanceEntries, studyInstanceUid, source.Token).Returns(storeResponse); - - var storeRequest = new StoreRequest(Stream.Null, DefaultContentType, studyInstanceUid); - - Assert.Equal( - storeResponse, - await _storeHandler.Handle(storeRequest, source.Token)); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Store/StoreOrchestrationFrameTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Store/StoreOrchestrationFrameTests.cs deleted file mode 100644 index 77996b2e41..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Store/StoreOrchestrationFrameTests.cs +++ /dev/null @@ -1,97 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.IO; -using System.Linq; -using FellowOakDicom; -using FellowOakDicom.Imaging; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Features.Store; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Store; -public class StoreOrchestrationFrameTests -{ - [Fact] - public void GivenDicom_WithNoImage_ReturnsNull() - { - // arrange - var ds = new DicomDataset - { - { DicomTag.SOPInstanceUID, DicomUID.Generate() }, - { DicomTag.SOPClassUID, DicomUID.SecondaryCaptureImageStorage }, - { DicomTag.ImageComments, " ".PadLeft(8000) } - }; - - // act - Dictionary ranges = StoreOrchestrator.GetFramesOffset(ds); - - // assert - Assert.Null(ranges); - } - - [Fact] - public void GivenDicom_WithFragmentFrame_ReturnsFrameRange() - { - // arrange - using Stream stream = new MemoryStream(Resource.layer1); - DicomFile dicomFile = DicomFile.Open(stream, FileReadOption.ReadLargeOnDemand, largeObjectSize: 1000); - - // act - Dictionary ranges = StoreOrchestrator.GetFramesOffset(dicomFile.Dataset); - - // assert - ValidateOffsetParser(ranges, dicomFile, Resource.layer1); - } - - [Fact] - public void GivenDicom_WithOtherByteFrame_ReturnsFrameRange() - { - // arrange - using Stream stream = new MemoryStream(Resource.red_triangle); - DicomFile dicomFile = DicomFile.Open(stream, FileReadOption.ReadLargeOnDemand, largeObjectSize: 1000); - - // act - Dictionary ranges = StoreOrchestrator.GetFramesOffset(dicomFile.Dataset); - - // assert - ValidateOffsetParser(ranges, dicomFile, Resource.red_triangle); - } - - [Fact] - public void GivenDicom_WithOtherWordFrame_ReturnsFrameRange() - { - // arrange - using Stream stream = new MemoryStream(Resource.case1_008); - DicomFile dicomFile = DicomFile.Open(stream, FileReadOption.ReadLargeOnDemand, largeObjectSize: 1000); - - // act - Dictionary ranges = StoreOrchestrator.GetFramesOffset(dicomFile.Dataset); - - // assert - ValidateOffsetParser(ranges, dicomFile, Resource.case1_008); - } - - private static void ValidateOffsetParser(Dictionary ranges, DicomFile file, byte[] originalStream) - { - foreach (var range in ranges) - { - DicomPixelData dicomPixelData = DicomPixelData.Create(file.Dataset); - var ebyteBuffer = dicomPixelData.GetFrame(range.Key); - byte[] abyteBuffer = originalStream.Skip((int)range.Value.Offset).Take((int)range.Value.Length).ToArray(); - Assert.True(ValidateStreamContent(abyteBuffer, ebyteBuffer.Data)); - } - } - - private static bool ValidateStreamContent(byte[] actual, byte[] expected) - { - if (actual.Length != expected.Length) - { - return false; - } - return actual.SequenceEqual(expected); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Store/StoreOrchestratorTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Store/StoreOrchestratorTests.cs deleted file mode 100644 index 123739f428..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Store/StoreOrchestratorTests.cs +++ /dev/null @@ -1,235 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using FellowOakDicom; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Context; -using Microsoft.Health.Dicom.Core.Features.Delete; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Core.Features.Store; -using Microsoft.Health.Dicom.Core.Features.Store.Entries; -using Microsoft.Health.Dicom.Tests.Common; -using NSubstitute; -using NSubstitute.ExceptionExtensions; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Store; - -public class StoreOrchestratorTests -{ - private const string DefaultStudyInstanceUid = "1"; - private const string DefaultSeriesInstanceUid = "2"; - private const string DefaultSopInstanceUid = "3"; - private const long DefaultVersion = 1; - - private static readonly VersionedInstanceIdentifier DefaultVersionedInstanceIdentifier = new VersionedInstanceIdentifier( - DefaultStudyInstanceUid, - DefaultSeriesInstanceUid, - DefaultSopInstanceUid, - DefaultVersion); - - private static readonly FileProperties DefaultFileProperties = new FileProperties - { - Path = "default/path/0.dcm", - ETag = "123", - ContentLength = 123 - }; - - private static readonly CancellationToken DefaultCancellationToken = new CancellationTokenSource().Token; - - private readonly IFileStore _fileStore = Substitute.For(); - private readonly IMetadataStore _metadataStore = Substitute.For(); - private readonly IIndexDataStore _indexDataStore = Substitute.For(); - private readonly IDeleteService _deleteService = Substitute.For(); - private readonly IQueryTagService _queryTagService = Substitute.For(); - private readonly IDicomRequestContextAccessor _contextAccessor = Substitute.For(); - private readonly IOptions _options = Substitute.For>(); - private readonly StoreOrchestrator _storeOrchestrator; - - private readonly DicomDataset _dicomDataset; - private readonly Stream _stream = new MemoryStream(); - private readonly IDicomInstanceEntry _dicomInstanceEntry = Substitute.For(); - private readonly List _eventInvocations = new List(); - private readonly List _queryTags = new List - { - new QueryTag(new ExtendedQueryTagStoreEntry(1, "00101010", "AS", null, QueryTagLevel.Study, ExtendedQueryTagStatus.Ready, QueryStatus.Enabled, 0)) - }; - - public StoreOrchestratorTests() - { - _dicomDataset = new DicomDataset() - { - { DicomTag.StudyInstanceUID, DefaultStudyInstanceUid }, - { DicomTag.SeriesInstanceUID, DefaultSeriesInstanceUid }, - { DicomTag.SOPInstanceUID, DefaultSopInstanceUid }, - }; - - _dicomInstanceEntry.GetDicomDatasetAsync(DefaultCancellationToken).Returns(_dicomDataset); - _dicomInstanceEntry.GetStreamAsync(DefaultCancellationToken).Returns(_stream); - - _indexDataStore - .BeginCreateInstanceIndexAsync(Arg.Any(), _dicomDataset, Arg.Any>(), DefaultCancellationToken) - .Returns(DefaultVersion); - - _queryTagService - .GetQueryTagsAsync(Arg.Any()) - .Returns(_queryTags); - - _contextAccessor.RequestContext.DataPartition = new Partition(1, "Microsoft.Default"); - var logger = NullLogger.Instance; - _options.Value.Returns(new FeatureConfiguration { EnableExternalStore = true, }); - _storeOrchestrator = new StoreOrchestrator( - _contextAccessor, - _fileStore, - _metadataStore, - _indexDataStore, - _deleteService, - _queryTagService, - _options, - logger); - } - - [Fact] - public async Task GivenFilesAreSuccessfullyStored_WhenStoringFile_ThenStatusShouldBeUpdatedToCreated() - { - _fileStore.StoreFileAsync( - DefaultVersionedInstanceIdentifier.Version, - DefaultVersionedInstanceIdentifier.Partition.Name, - _stream, - cancellationToken: DefaultCancellationToken) - .Returns(DefaultFileProperties); - - await _storeOrchestrator.StoreDicomInstanceEntryAsync(_dicomInstanceEntry, DefaultCancellationToken); - - await ValidateStatusUpdateAsync(); - } - - [Fact] - public async Task GivenFilesAreSuccessfullyStored_WhenStoringFileWithFragmentPixelData_ThenStatusIsUpdatedToCreatedAndHasFrameDataSetToTrue() - { - var studyInstanceUid = TestUidGenerator.Generate(); - var seriesInstanceUid = TestUidGenerator.Generate(); - var sopInstanceUid = TestUidGenerator.Generate(); - - using (var stream = new MemoryStream()) - { - DicomFile dicomFile = Samples.CreateRandomDicomFileWithFragmentPixelData( - studyInstanceUid, - seriesInstanceUid, - sopInstanceUid, - rows: 1, - columns: 1, - frames: 1); - - await dicomFile.SaveAsync(stream); - - _indexDataStore - .BeginCreateInstanceIndexAsync(Arg.Any(), dicomFile.Dataset, Arg.Any>(), DefaultCancellationToken) - .Returns(DefaultVersion); - _dicomInstanceEntry.GetDicomDatasetAsync(DefaultCancellationToken).Returns(dicomFile.Dataset); - _dicomInstanceEntry.GetStreamAsync(DefaultCancellationToken).Returns(stream); - - _fileStore.StoreFileAsync( - Arg.Any(), - DefaultVersionedInstanceIdentifier.Partition.Name, - Arg.Any(), - cancellationToken: DefaultCancellationToken) - .Returns(DefaultFileProperties); - - await _storeOrchestrator.StoreDicomInstanceEntryAsync(_dicomInstanceEntry, DefaultCancellationToken); - - await ValidateStatusUpdateAsync(_queryTags, hasFrameMetadata: true, dicomFile.Dataset); - } - } - - [Fact] - public async Task GivenFailedToStoreFile_WhenStoringFile_ThenCleanupShouldBeAttempted() - { - _fileStore.StoreFileAsync( - DefaultVersionedInstanceIdentifier.Version, - DefaultVersionedInstanceIdentifier.Partition.Name, - _stream, - cancellationToken: DefaultCancellationToken) - .Throws(new Exception()); - - _indexDataStore.ClearReceivedCalls(); - - await Assert.ThrowsAsync(() => _storeOrchestrator.StoreDicomInstanceEntryAsync(_dicomInstanceEntry, DefaultCancellationToken)); - - await ValidateCleanupAsync(); - - await _indexDataStore.DidNotReceiveWithAnyArgs().EndCreateInstanceIndexAsync(default, default, default, default, default, default); - } - - [Fact] - public async Task GivenFailedToStoreMetadataFile_WhenStoringMetadata_ThenCleanupShouldBeAttempted() - { - _metadataStore.StoreInstanceMetadataAsync( - _dicomDataset, - DefaultVersion, - DefaultCancellationToken) - .Throws(new Exception()); - - _indexDataStore.ClearReceivedCalls(); - - await Assert.ThrowsAsync(() => _storeOrchestrator.StoreDicomInstanceEntryAsync(_dicomInstanceEntry, DefaultCancellationToken)); - - await ValidateCleanupAsync(); - - await _indexDataStore.DidNotReceiveWithAnyArgs().EndCreateInstanceIndexAsync(default, default, default, default, default, default); - } - - [Fact] - public async Task GivenExceptionDuringCleanup_WhenStoreDicomInstanceEntryIsCalled_ThenItShouldNotInterfere() - { - _metadataStore.StoreInstanceMetadataAsync( - _dicomDataset, - DefaultVersion, - DefaultCancellationToken) - .Throws(new ArgumentException()); - - _indexDataStore.DeleteInstanceIndexAsync(default, default, default, default, default).ThrowsForAnyArgs(new InvalidOperationException()); - - await Assert.ThrowsAsync(() => _storeOrchestrator.StoreDicomInstanceEntryAsync(_dicomInstanceEntry, DefaultCancellationToken)); - } - - private Task ValidateStatusUpdateAsync() - => ValidateStatusUpdateAsync(_queryTags); - - private Task ValidateStatusUpdateAsync(IEnumerable expectedTags, bool hasFrameMetadata = false, DicomDataset dataset = null) - => _indexDataStore - .Received(1) - .EndCreateInstanceIndexAsync( - 1, - dataset ?? _dicomDataset, - DefaultVersion, - expectedTags, - fileProperties: Arg.Is( - p => p.Path == DefaultFileProperties.Path - && p.ETag == DefaultFileProperties.ETag - && p.ContentLength == DefaultFileProperties.ContentLength), - allowExpiredTags: false, - hasFrameMetadata: hasFrameMetadata, - cancellationToken: DefaultCancellationToken); - - private Task ValidateCleanupAsync() - => _deleteService - .Received(1) - .DeleteInstanceNowAsync( - DefaultStudyInstanceUid, - DefaultSeriesInstanceUid, - DefaultSopInstanceUid, - CancellationToken.None); -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Store/StoreRequestValidatorTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Store/StoreRequestValidatorTests.cs deleted file mode 100644 index c4ee01e615..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Store/StoreRequestValidatorTests.cs +++ /dev/null @@ -1,44 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.IO; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Store; -using Microsoft.Health.Dicom.Core.Messages.Store; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Store; - -public class StoreRequestValidatorTests -{ - [Fact] - public void GivenNullRequestBody_WhenValidated_ThenBadRequestExceptionShouldBeThrown() - { - StoreRequest request = new StoreRequest(null, "application/dicom"); - - Assert.Throws(() => StoreRequestValidator.ValidateRequest(request)); - } - - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData("1.2.3")] - public void GivenAValidStudyInstanceId_WhenValidated_ThenItShouldSucceed(string studyInstanceUid) - { - StoreRequest request = new StoreRequest(Stream.Null, "application/dicom", studyInstanceUid); - - StoreRequestValidator.ValidateRequest(request); - } - - [Theory] - [InlineData("1.a1.2")] - [InlineData("invalid")] - public void GivenAnInvalidStudyInstanceUid_WhenValidated_ThenInvalidIdentifierExceptionShouldBeThrown(string studyInstanceUid) - { - StoreRequest request = new StoreRequest(Stream.Null, "application/dicom", studyInstanceUid); - - Assert.Throws(() => StoreRequestValidator.ValidateRequest(request)); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Store/StoreResponseBuilderTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Store/StoreResponseBuilderTests.cs deleted file mode 100644 index 928463bf3d..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Store/StoreResponseBuilderTests.cs +++ /dev/null @@ -1,356 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Linq; -using FellowOakDicom; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Core.Features.Routing; -using Microsoft.Health.Dicom.Core.Features.Store; -using Microsoft.Health.Dicom.Core.Messages.Store; -using Microsoft.Health.Dicom.Tests.Common; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Store; - -public class StoreResponseBuilderTests -{ - private readonly IUrlResolver _urlResolver = new MockUrlResolver(); - private readonly StoreResponseBuilder _storeResponseBuilder; - private static readonly StoreValidationResult DefaultStoreValidationResult = new StoreValidationResultBuilder().Build(); - - private readonly DicomDataset _dicomDataset1 = Samples.CreateRandomInstanceDataset( - studyInstanceUid: "1", - seriesInstanceUid: "2", - sopInstanceUid: "3", - sopClassUid: "4"); - - private readonly DicomDataset _dicomDataset2 = Samples.CreateRandomInstanceDataset( - studyInstanceUid: "10", - seriesInstanceUid: "11", - sopInstanceUid: "12", - sopClassUid: "13"); - - private readonly IOptions _options; - - - public StoreResponseBuilderTests() - { - _options = Substitute.For>(); - _options.Value.Returns(new FeatureConfiguration - { - EnableDataPartitions = false, - }); - _storeResponseBuilder = new StoreResponseBuilder( - _urlResolver, - _options); - } - - [Theory] - [InlineData(null)] - [InlineData("1.2.3")] - public void GivenNoEntries_WhenResponseIsBuilt_ThenCorrectResponseShouldBeReturned(string studyInstanceUid) - { - StoreResponse response = _storeResponseBuilder.BuildResponse(studyInstanceUid); - - Assert.NotNull(response); - Assert.Equal(StoreResponseStatus.None, response.Status); - Assert.Null(response.Dataset); - } - - [Fact] - public void GivenOnlySuccessEntry_WhenResponseIsBuilt_ThenCorrectResponseShouldBeReturned() - { - _storeResponseBuilder.AddSuccess(_dicomDataset1, DefaultStoreValidationResult, Partition.Default); - - StoreResponse response = _storeResponseBuilder.BuildResponse(null); - - Assert.NotNull(response); - Assert.Equal(StoreResponseStatus.Success, response.Status); - Assert.NotNull(response.Dataset); - Assert.Single(response.Dataset); - - ValidationHelpers.ValidateReferencedSopSequence( - response.Dataset, - ("3", "/1/2/3", "4")); - } - - [Fact] - public void GivenOnlySuccessEntry_WhenBuildWarningSequenceEnabledAndHasWarningCode_ThenExpectPartialSuccessReturned() - { - _storeResponseBuilder.AddSuccess( - _dicomDataset1, - DefaultStoreValidationResult, - Partition.Default, - warningReasonCode: WarningReasonCodes.DatasetHasValidationWarnings); - - StoreResponse response = _storeResponseBuilder.BuildResponse(null, returnWarning202: true); - - Assert.NotNull(response); - Assert.Equal(StoreResponseStatus.PartialSuccess, response.Status); - } - - [Fact] - public void GivenOnlySuccessEntry_WhenBuildWarningSequenceEnabledAndHasNoWarningCode_ThenExpectSuccessReturned() - { - _storeResponseBuilder.AddSuccess( - _dicomDataset1, - DefaultStoreValidationResult, - Partition.Default, - warningReasonCode: null); - - StoreResponse response = _storeResponseBuilder.BuildResponse(null, returnWarning202: true); - - Assert.NotNull(response); - Assert.Equal(StoreResponseStatus.Success, response.Status); - } - - [Fact] - public void GivenBuilderHadNoErrors_WhenBuildWarningSequenceEnabled_ThenResponseHasEmptyFailedSequence() - { - _storeResponseBuilder.AddSuccess(_dicomDataset1, DefaultStoreValidationResult, Partition.Default, buildWarningSequence: true); - - StoreResponse response = _storeResponseBuilder.BuildResponse(null); - - Assert.Single(response.Dataset); - - DicomSequence refSopSequence = response.Dataset.GetSequence(DicomTag.ReferencedSOPSequence); - Assert.Single(refSopSequence); - DicomDataset ds = refSopSequence.Items[0]; - - DicomSequence failedSequence = ds.GetSequence(DicomTag.FailedAttributesSequence); - Assert.Empty(failedSequence); - } - - [Fact] - public void GivenOnlyFailedEntry_WhenResponseIsBuilt_ThenCorrectResponseShouldBeReturned() - { - const ushort failureReasonCode = 100; - - _storeResponseBuilder.AddFailure(_dicomDataset2, failureReasonCode); - - StoreResponse response = _storeResponseBuilder.BuildResponse(null); - - Assert.NotNull(response); - Assert.Equal(StoreResponseStatus.Failure, response.Status); - Assert.NotNull(response.Dataset); - Assert.Single(response.Dataset); - - ValidationHelpers.ValidateFailedSopSequence( - response.Dataset, - ("12", "13", failureReasonCode)); - } - - [Fact] - public void GivenBuilderHasErrors_WhenBuildWarningSequenceEnabled_ThenResponseHasNonEmptyFailedSequence() - { - StoreValidationResultBuilder builder = new StoreValidationResultBuilder(); - builder.Add(new Exception("There was an issue with an attribute"), DicomTag.PatientAge); - StoreValidationResult storeValidationResult = builder.Build(); - - _storeResponseBuilder.AddSuccess(_dicomDataset1, storeValidationResult, Partition.Default, buildWarningSequence: true); - - StoreResponse response = _storeResponseBuilder.BuildResponse(null); - - Assert.Single(response.Dataset); - - DicomSequence refSopSequence = response.Dataset.GetSequence(DicomTag.ReferencedSOPSequence); - Assert.Single(refSopSequence); - DicomDataset ds = refSopSequence.Items[0]; - - DicomSequence failedSequence = ds.GetSequence(DicomTag.FailedAttributesSequence); - Assert.Single(failedSequence); - // expect comment sequence has single warning about single invalid attribute - Assert.Equal( - storeValidationResult.InvalidTagErrors.ToArray()[0].Value.Error, - failedSequence.Items[0].GetString(DicomTag.ErrorComment) - ); - } - - [Fact] - public void GivenBuildWithAndWithoutErrors_WhenBuildWarningSequenceEnabled_ThenResponseHasNonEmptyFailedSequenceAndEmptyFailedSequence() - { - // This represents multiple instance being processed where one had a validation failure and the other did not - - // simulate validation failure - StoreValidationResultBuilder builder = new StoreValidationResultBuilder(); - builder.Add(new Exception("There was an issue with an attribute"), DicomTag.PatientAge); - StoreValidationResult storeValidationResult = builder.Build(); - _storeResponseBuilder.AddSuccess(_dicomDataset1, storeValidationResult, Partition.Default, buildWarningSequence: true); - - //simulate validation pass - _storeResponseBuilder.AddSuccess(_dicomDataset1, DefaultStoreValidationResult, Partition.Default, buildWarningSequence: true); - - StoreResponse response = _storeResponseBuilder.BuildResponse(null); - - Assert.NotNull(response.Dataset); - - DicomSequence refSopSequence = response.Dataset.GetSequence(DicomTag.ReferencedSOPSequence); - Assert.Equal(2, refSopSequence.Items.Count); - - // invalid instance section has error in FailedSOPSequence - DicomDataset invalidInstanceResponse = refSopSequence.Items[0]; - DicomSequence failedSequence = invalidInstanceResponse.GetSequence(DicomTag.FailedAttributesSequence); - Assert.Single(failedSequence); - // expect comment sequence has single warning about single invalid attribute - Assert.Equal( - storeValidationResult.InvalidTagErrors.ToArray()[0].Value.Error, - failedSequence.Items[0].GetString(DicomTag.ErrorComment) - ); - - // valid instance section has an empty FailedSOPSequence as there were no errors - DicomDataset validInstanceResponse = refSopSequence.Items[1]; - Assert.Empty(validInstanceResponse.GetSequence(DicomTag.FailedAttributesSequence)); - } - - [Fact] - public void GivenBothSuccessAndFailedEntries_WhenResponseIsBuilt_ThenCorrectResponseShouldBeReturned() - { - _storeResponseBuilder.AddFailure(_dicomDataset1, TestConstants.ProcessingFailureReasonCode); - _storeResponseBuilder.AddSuccess(_dicomDataset2, DefaultStoreValidationResult, Partition.Default); - - StoreResponse response = _storeResponseBuilder.BuildResponse(null); - - Assert.NotNull(response); - Assert.Equal(StoreResponseStatus.PartialSuccess, response.Status); - Assert.NotNull(response.Dataset); - Assert.Equal(2, response.Dataset.Count()); - - ValidationHelpers.ValidateFailedSopSequence( - response.Dataset, - ("3", "4", TestConstants.ProcessingFailureReasonCode)); - - ValidationHelpers.ValidateReferencedSopSequence( - response.Dataset, - ("12", "/10/11/12", "13")); - } - - [Fact] - public void GivenMultipleSuccessAndFailedEntries_WhenResponseIsBuilt_ThenCorrectResponseShouldBeReturned() - { - ushort failureReasonCode1 = TestConstants.ProcessingFailureReasonCode; - ushort failureReasonCode2 = 100; - - _storeResponseBuilder.AddFailure(_dicomDataset1, failureReasonCode1); - _storeResponseBuilder.AddFailure(_dicomDataset2, failureReasonCode2); - - _storeResponseBuilder.AddSuccess(_dicomDataset2, DefaultStoreValidationResult, Partition.Default); - _storeResponseBuilder.AddSuccess(_dicomDataset1, DefaultStoreValidationResult, Partition.Default); - - StoreResponse response = _storeResponseBuilder.BuildResponse(null); - - Assert.NotNull(response); - Assert.Equal(StoreResponseStatus.PartialSuccess, response.Status); - Assert.NotNull(response.Dataset); - Assert.Equal(2, response.Dataset.Count()); - - ValidationHelpers.ValidateFailedSopSequence( - response.Dataset, - ("3", "4", failureReasonCode1), - ("12", "13", failureReasonCode2)); - - ValidationHelpers.ValidateReferencedSopSequence( - response.Dataset, - ("12", "/10/11/12", "13"), - ("3", "/1/2/3", "4")); - } - - [Fact] - public void GivenNullDicomDatasetWhenAddingFailure_WhenResponseIsBuilt_ThenCorrectResponseShouldBeReturned() - { - const ushort failureReasonCode = 300; - - _storeResponseBuilder.AddFailure(dicomDataset: null, failureReasonCode: failureReasonCode); - - StoreResponse response = _storeResponseBuilder.BuildResponse(null); - - Assert.NotNull(response); - Assert.Equal(StoreResponseStatus.Failure, response.Status); - Assert.NotNull(response.Dataset); - Assert.Single(response.Dataset); - - ValidationHelpers.ValidateFailedSopSequence( - response.Dataset, - (null, null, failureReasonCode)); - } - - [Fact] - public void GivenStudyInstanceUidAndThereIsOnlySuccessEntries_WhenResponseIsBuilt_ThenCorrectResponseShouldBeReturned() - { - _storeResponseBuilder.AddSuccess(_dicomDataset1, DefaultStoreValidationResult, Partition.Default); - - StoreResponse response = _storeResponseBuilder.BuildResponse("1"); - - Assert.NotNull(response); - Assert.NotNull(response.Dataset); - - // We have 2 items: RetrieveURL and ReferencedSOPSequence. - Assert.Equal(2, response.Dataset.Count()); - Assert.Equal("1", response.Dataset.GetFirstValueOrDefault(DicomTag.RetrieveURL)); - } - - [Fact] - public void GivenStudyInstanceUidAndThereIsOnlyFailedEntries_WhenResponseIsBuilt_ThenCorrectResponseShouldBeReturned() - { - _storeResponseBuilder.AddFailure(dicomDataset: null, failureReasonCode: 500); - - StoreResponse response = _storeResponseBuilder.BuildResponse("1"); - - Assert.NotNull(response); - Assert.NotNull(response.Dataset); - - // We have 1 item: FailedSOPSequence. - Assert.Single(response.Dataset); - } - - [Fact] - public void GivenStudyInstanceUidAndThereAreSuccessAndFailureEntries_WhenResponseIsBuilt_ThenCorrectResponseShouldBeReturned() - { - _storeResponseBuilder.AddSuccess(_dicomDataset1, DefaultStoreValidationResult, Partition.Default); - _storeResponseBuilder.AddFailure(_dicomDataset2, failureReasonCode: 200); - - StoreResponse response = _storeResponseBuilder.BuildResponse("1"); - - Assert.NotNull(response); - Assert.NotNull(response.Dataset); - - // We have 3 items: RetrieveURL, FailedSOPSequence, and ReferencedSOPSequence. - Assert.Equal(3, response.Dataset.Count()); - Assert.Equal("1", response.Dataset.GetFirstValueOrDefault(DicomTag.RetrieveURL)); - } - - [Fact] - public void GivenInvalidUidValue_WhenResponseIsBuilt_ThenItShouldNotThrowException() - { - // Create a DICOM dataset with invalid UID value. - var dicomDataset = new DicomDataset().NotValidated(); - - dicomDataset.Add(DicomTag.SOPClassUID, "invalid"); - - _storeResponseBuilder.AddFailure(dicomDataset, failureReasonCode: 500); - - StoreResponse response = _storeResponseBuilder.BuildResponse(studyInstanceUid: null); - - Assert.NotNull(response); - Assert.NotNull(response.Dataset); - - ValidationHelpers.ValidateFailedSopSequence( - response.Dataset, - (null, "invalid", 500)); - } - - [Fact] - public void GivenWarning_WhenResponseIsBuilt_ThenItShouldHaveExpectedWarning() - { - string warning = "WarningMessage"; - _storeResponseBuilder.SetWarningMessage(warning); - var response = _storeResponseBuilder.BuildResponse(studyInstanceUid: null); - Assert.Equal(warning, response.Warning); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Store/TestConstants.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Store/TestConstants.cs deleted file mode 100644 index 4d357defef..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Store/TestConstants.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Store; - -internal static class TestConstants -{ - public const ushort ProcessingFailureReasonCode = 272; - public const ushort ValidationFailureReasonCode = 43264; - public const ushort MismatchStudyInstanceUidReasonCode = 43265; - public const ushort SopInstanceAlreadyExistsReasonCode = 45070; -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Update/UpdateInstanceHandlerTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Update/UpdateInstanceHandlerTests.cs deleted file mode 100644 index 54e070eacd..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Update/UpdateInstanceHandlerTests.cs +++ /dev/null @@ -1,80 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using FellowOakDicom; -using Microsoft.Health.Core.Features.Security.Authorization; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Routing; -using Microsoft.Health.Dicom.Core.Features.Security; -using Microsoft.Health.Dicom.Core.Features.Update; -using Microsoft.Health.Dicom.Core.Messages.Update; -using Microsoft.Health.Dicom.Core.Models.Update; -using Microsoft.Health.Dicom.Tests.Common; -using Microsoft.Health.Operations; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Update; - -public class UpdateInstanceHandlerTests -{ - private const string DefaultContentType = "application/json"; - private readonly UpdateInstanceHandler _handler; - private readonly IUpdateInstanceOperationService _updateInstanceOperationService; - private readonly IAuthorizationService _auth; - - public UpdateInstanceHandlerTests() - { - _updateInstanceOperationService = Substitute.For(); - _auth = Substitute.For>(); - _handler = new UpdateInstanceHandler(_auth, _updateInstanceOperationService); - } - - [Fact] - public async Task GivenNullRequestBody_WhenHandled_ThenArgumentNullExceptionShouldBeThrown() - { - var updateInstanceRequest = new UpdateInstanceRequest(null); - _auth.CheckAccess(DataActions.Write, CancellationToken.None).Returns(DataActions.Write); - await Assert.ThrowsAsync(() => _handler.Handle(updateInstanceRequest, CancellationToken.None)); - } - - [Fact] - public async Task GivenNoAccess_WhenHandlingRequest_ThenThrowUnauthorizedDicomActionException() - { - IReadOnlyList studyInstanceUids = new List() { "1.2.3.4" }; - DicomDataset changeDataset = new DicomDataset(); - var updateInstanceRequest = new UpdateInstanceRequest(new UpdateSpecification(studyInstanceUids, changeDataset)); - _auth.CheckAccess(DataActions.Write, CancellationToken.None).Returns(DataActions.None); - await Assert.ThrowsAsync(() => _handler.Handle(updateInstanceRequest, CancellationToken.None)); - - await _auth.Received(1).CheckAccess(DataActions.Write, CancellationToken.None); - await _updateInstanceOperationService.DidNotReceiveWithAnyArgs().QueueUpdateOperationAsync(default, default); - } - - [Fact] - public async Task GivenSupportedContentType_WhenHandled_ThenCorrectUpdateInstanceResponseShouldBeReturned() - { - var id = Guid.NewGuid(); - IUrlResolver urlResolver = new MockUrlResolver(); - IReadOnlyList studyInstanceUids = new List() { "1.2.3.4" }; - DicomDataset changeDataset = new DicomDataset(); - var updateSpec = new UpdateSpecification(studyInstanceUids, changeDataset); - var operation = new OperationReference(id, urlResolver.ResolveOperationStatusUri(id)); - var updateInstanceRequest = new UpdateInstanceRequest(updateSpec); - var updateInstanceResponse = new UpdateInstanceResponse(operation); - _auth.CheckAccess(DataActions.Write, CancellationToken.None).Returns(DataActions.Write); - - _updateInstanceOperationService.QueueUpdateOperationAsync(updateSpec, CancellationToken.None).Returns(updateInstanceResponse); - - var response = await _handler.Handle(updateInstanceRequest, CancellationToken.None); - - await _auth.Received(1).CheckAccess(DataActions.Write, CancellationToken.None); - Assert.Equal(operation, response.Operation); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Update/UpdateInstanceOperationServiceTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Update/UpdateInstanceOperationServiceTests.cs deleted file mode 100644 index 6e804939b0..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Update/UpdateInstanceOperationServiceTests.cs +++ /dev/null @@ -1,105 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using FellowOakDicom; -using FellowOakDicom.Serialization; -using Microsoft.ApplicationInsights; -using Microsoft.ApplicationInsights.Channel; -using Microsoft.ApplicationInsights.Extensibility; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Context; -using Microsoft.Health.Dicom.Core.Features.Operations; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Core.Features.Update; -using Microsoft.Health.Dicom.Core.Models.Operations; -using Microsoft.Health.Dicom.Core.Models.Update; -using Microsoft.Health.Dicom.Core.Serialization; -using Microsoft.Health.Operations; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Update; -public class UpdateInstanceOperationServiceTests -{ - private readonly IUpdateInstanceOperationService _updateInstanceOperationService; - private readonly IGuidFactory _guidFactory; - private readonly IDicomOperationsClient _client; - private readonly IDicomRequestContextAccessor _contextAccessor; - private readonly TelemetryClient _telemetryClient; - private readonly JsonSerializerOptions _jsonSerializerOptions; - - public UpdateInstanceOperationServiceTests() - { - _guidFactory = Substitute.For(); - _client = Substitute.For(); - _contextAccessor = Substitute.For(); - _telemetryClient = new TelemetryClient(new TelemetryConfiguration() - { - TelemetryChannel = Substitute.For(), - }); - _jsonSerializerOptions = new JsonSerializerOptions(); - _jsonSerializerOptions.Converters.Add(new DicomJsonConverter(writeTagsAsKeywords: true, autoValidate: false, numberSerializationMode: NumberSerializationMode.PreferablyAsNumber)); - _jsonSerializerOptions.Converters.Add(new ExportDataOptionsJsonConverter()); - _updateInstanceOperationService = new UpdateInstanceOperationService(_guidFactory, _client, _contextAccessor, _telemetryClient, Options.Create(_jsonSerializerOptions), NullLogger.Instance); - } - - [Fact] - public async Task WhenExistingOperationQueued_ThenExistingOperationExceptionShouldBeThrown() - { - IReadOnlyList studyInstanceUids = new List() { "1.2.3.4" }; - DicomDataset changeDataset = new DicomDataset(); - var updateSpec = new UpdateSpecification(studyInstanceUids, changeDataset); - var id = Guid.NewGuid(); - var expected = new OperationReference(id, new Uri("https://dicom.contoso.io/unit/test/Operations/" + id, UriKind.Absolute)); - - _client.FindOperationsAsync(Arg.Is(GetOperationPredicate()), CancellationToken.None) - .Returns(new OperationReference[] { expected }.ToAsyncEnumerable()); - await Assert.ThrowsAsync(() => - _updateInstanceOperationService.QueueUpdateOperationAsync(updateSpec, CancellationToken.None)); - } - - [Fact] - public async Task GivenValidInput_WhenNoExistingOperationQueued_ThenShouldSucceed() - { - IReadOnlyList studyInstanceUids = new List() { "1.2.3.4" }; - DicomDataset changeDataset = new DicomDataset(); - var updateSpec = new UpdateSpecification(studyInstanceUids, changeDataset); - var operationId = Guid.NewGuid(); - var expected = new OperationReference(operationId, new Uri("https://dicom.contoso.io/unit/test/Operations/" + operationId, UriKind.Absolute)); - - _client.FindOperationsAsync(Arg.Is(GetOperationPredicate()), CancellationToken.None) - .Returns(AsyncEnumerable.Empty()); - - _client - .StartUpdateOperationAsync( - Arg.Any(), - Arg.Any(), - Partition.Default, - CancellationToken.None) - .Returns(expected); - - _contextAccessor.RequestContext.DataPartition = Partition.Default; - var response = await _updateInstanceOperationService.QueueUpdateOperationAsync(updateSpec, CancellationToken.None); - - Assert.Equal(expected.Href.ToString(), response.Operation.Href.ToString()); - } - - private static Expression>> GetOperationPredicate() - => (x) => - x.CreatedTimeFrom == DateTime.MinValue && - x.CreatedTimeTo == DateTime.MaxValue && - x.Operations.Single() == DicomOperation.Update && - x.Statuses.SequenceEqual(new OperationStatus[] { OperationStatus.NotStarted, OperationStatus.Running }); -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Update/UpdateInstanceServiceTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Update/UpdateInstanceServiceTests.cs deleted file mode 100644 index b72b7f49db..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Update/UpdateInstanceServiceTests.cs +++ /dev/null @@ -1,342 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using FellowOakDicom; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Context; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Core.Features.Update; -using Microsoft.Health.Dicom.Core.UnitTests.Features.Retrieve; -using Microsoft.Health.Dicom.Tests.Common; -using Microsoft.IO; -using NSubstitute; -using Xunit; -using DicomFileExtensions = Microsoft.Health.Dicom.Core.Features.Retrieve.DicomFileExtensions; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Update; - -public class UpdateInstanceServiceTests -{ - private readonly IFileStore _fileStore; - private readonly ILogger _logger; - private readonly IMetadataStore _metadataStore; - private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager; - private readonly UpdateInstanceService _updateInstanceService; - private readonly IDicomRequestContextAccessor _dicomRequestContextAccessor; - private readonly UpdateConfiguration _config; - private static readonly FileProperties DefaultFileProperties = new() - { - Path = "default/path/0.dcm", - ETag = "123", - ContentLength = 123 - }; - private static readonly FileProperties DefaultCopiedFileProperties = new() - { - Path = "default/path/1.dcm", - ETag = "456", - ContentLength = 456 - }; - private static readonly FileProperties DefaultUpdatedFileProperties = new() - { - Path = "default/path/1.dcm", - ETag = "789", - ContentLength = 789 - }; - private static readonly InstanceMetadata DefaultInstanceMetadata = new InstanceMetadata( - new VersionedInstanceIdentifier( - TestUidGenerator.Generate(), - TestUidGenerator.Generate(), - TestUidGenerator.Generate(), - 1L, - Partition.Default), - new InstanceProperties()); - - public UpdateInstanceServiceTests() - { - _fileStore = Substitute.For(); - _metadataStore = Substitute.For(); - _recyclableMemoryStreamManager = new RecyclableMemoryStreamManager(); - _logger = NullLogger.Instance; - _dicomRequestContextAccessor = Substitute.For(); - _dicomRequestContextAccessor.RequestContext.DataPartition = Partition.Default; - _config = new UpdateConfiguration(); - - _updateInstanceService = new UpdateInstanceService( - _fileStore, - _metadataStore, - _recyclableMemoryStreamManager, - Options.Create(_config), - _logger); - } - - [Fact] - public async Task GivenDatasetToUpdateIsNull_WhenCalled_ThrowsArgumentNullException() - { - await Assert.ThrowsAsync(() => - _updateInstanceService.UpdateInstanceBlobAsync( - DefaultInstanceMetadata, - null, - Partition.Default, - CancellationToken.None)); - } - - [Fact] - public async Task GivenInstanceFileIdentifierIsNull_WhenCalled_ThrowsArgumentNullException() - { - DicomDataset datasetToUpdate = new DicomDataset(); - await Assert.ThrowsAsync(() => - _updateInstanceService.UpdateInstanceBlobAsync(null, datasetToUpdate, Partition.Default, CancellationToken.None)); - } - - [Fact] - public async Task GivenNewVersionIsNull_WhenCalled_ThrowsArgumentException() - { - Assert.Null(DefaultInstanceMetadata.InstanceProperties.NewVersion); - DicomDataset datasetToUpdate = new DicomDataset(); - await Assert.ThrowsAsync(() => - _updateInstanceService.UpdateInstanceBlobAsync(DefaultInstanceMetadata, datasetToUpdate, Partition.Default, CancellationToken.None)); - } - - [Fact] - public async Task GivenValidInput_WhenDeletingBothFileAndMetadata_ThenItDeletes() - { - long fileIdentifier = 1234; - await _updateInstanceService.DeleteInstanceBlobAsync(fileIdentifier, Partition.Default, DefaultFileProperties); - await _fileStore.Received(1).DeleteFileIfExistsAsync(fileIdentifier, Partition.Default, DefaultFileProperties, CancellationToken.None); - await _metadataStore.Received(1).DeleteInstanceMetadataIfExistsAsync(fileIdentifier, CancellationToken.None); - } - - [Fact] - public async Task GivenValidInput_WhenCallingUpdateBlobAsync_ShouldCallUpdateInstanceFileAsync_AndUpdateInstanceMetadataAsync() - { - // data setup - long fileIdentifier = 123; - long newFileIdentifier = 456; - List versionedInstanceIdentifiers = SetupInstanceIdentifiersList(version: fileIdentifier, newVersion: newFileIdentifier); - var datasetToUpdate = new DicomDataset(); - var cancellationToken = CancellationToken.None; - - KeyValuePair streamAndStoredFile = await RetrieveHelpers.StreamAndStoredFileFromDataset( - RetrieveHelpers.GenerateDatasetsFromIdentifiers( - versionedInstanceIdentifiers.First().VersionedInstanceIdentifier), - _recyclableMemoryStreamManager, - frames: 3); - - MemoryStream copyStream = _recyclableMemoryStreamManager.GetStream(); - await streamAndStoredFile.Value.CopyToAsync(copyStream); - copyStream.Position = 0; - streamAndStoredFile.Value.Position = 0; - - var binaryData = await BinaryData.FromStreamAsync(copyStream); - copyStream.Position = 0; - - // all calls for updating the blob - _fileStore.GetFileAsync(fileIdentifier, Partition.Default, DefaultFileProperties, cancellationToken).Returns(streamAndStoredFile.Value); - _fileStore.StoreFileInBlocksAsync( - newFileIdentifier, - Partition.Default, - Arg.Any(), - _config.StageBlockSizeInBytes, - Arg.Any>(), - cancellationToken) - .Returns(DefaultCopiedFileProperties); - _fileStore.GetFileContentInRangeAsync(newFileIdentifier, Partition.Default, DefaultCopiedFileProperties, Arg.Any(), cancellationToken).Returns(binaryData); - _fileStore.UpdateFileBlockAsync(newFileIdentifier, Partition.Default, DefaultCopiedFileProperties, Arg.Any(), Arg.Any(), cancellationToken).Returns(DefaultUpdatedFileProperties); - - // calls for updating the metadata - _metadataStore.GetInstanceMetadataAsync(fileIdentifier, cancellationToken).Returns(streamAndStoredFile.Key.Dataset); - _metadataStore.StoreInstanceMetadataAsync(streamAndStoredFile.Key.Dataset, newFileIdentifier, cancellationToken).Returns(Task.CompletedTask); - - // test - FileProperties returnedFileProperties = await _updateInstanceService.UpdateInstanceBlobAsync(versionedInstanceIdentifiers.First(), datasetToUpdate, Partition.Default, cancellationToken); - - // assert - // file properties with updated etag of copied file returned external store is enabled - Assert.Equal(DefaultUpdatedFileProperties.Path, returnedFileProperties.Path); - Assert.Equal(DefaultUpdatedFileProperties.ETag, returnedFileProperties.ETag); - // since our file had not been previously copied, we create a new file - await _fileStore.Received(1).GetFileAsync(fileIdentifier, Partition.Default, DefaultFileProperties, cancellationToken); - // all calls expected as received - await _fileStore.Received(1).GetFileAsync(fileIdentifier, Partition.Default, DefaultFileProperties, cancellationToken); - await _fileStore.Received(1).StoreFileInBlocksAsync( - newFileIdentifier, - Partition.Default, - Arg.Any(), - _config.StageBlockSizeInBytes, - Arg.Any>(), - cancellationToken); - await _fileStore.Received(1).GetFileContentInRangeAsync(newFileIdentifier, Partition.Default, DefaultCopiedFileProperties, Arg.Any(), cancellationToken); - _fileStore.UpdateFileBlockAsync(newFileIdentifier, Partition.Default, DefaultCopiedFileProperties, Arg.Any(), Arg.Any(), cancellationToken).Returns(DefaultUpdatedFileProperties); - await _fileStore.Received(1).UpdateFileBlockAsync(newFileIdentifier, Partition.Default, DefaultCopiedFileProperties, Arg.Any(), Arg.Any(), cancellationToken); - await _metadataStore.Received(1).GetInstanceMetadataAsync(fileIdentifier, cancellationToken); - await _metadataStore.Received(1).StoreInstanceMetadataAsync(streamAndStoredFile.Key.Dataset, newFileIdentifier, cancellationToken); - // since our file had not been previously copied, we do not just update an already existing file - await _fileStore.DidNotReceive().CopyFileAsync(fileIdentifier, newFileIdentifier, Partition.Default, DefaultFileProperties, cancellationToken); - // instead, we create a new file - await _fileStore.DidNotReceive().CopyFileAsync(fileIdentifier, newFileIdentifier, Partition.Default, DefaultCopiedFileProperties, cancellationToken); - // cleanup - streamAndStoredFile.Value.Dispose(); - copyStream.Dispose(); - } - - [Fact] - public async Task GivenValidInputWithLargeFile_WhenCallingUpdateBlobAsync_ShouldCallUpdateInstanceFileAsync_AndUpdateInstanceMetadataAsync() - { - long fileIdentifier = 123; - long newFileIdentifier = 456; - List versionedInstanceIdentifiers = SetupInstanceIdentifiersList(version: fileIdentifier, newVersion: newFileIdentifier); - var datasetToUpdate = new DicomDataset(); - var cancellationToken = CancellationToken.None; - - KeyValuePair streamAndStoredFile = await RetrieveHelpers.StreamAndStoredFileFromDataset( - RetrieveHelpers.GenerateDatasetsFromIdentifiers( - versionedInstanceIdentifiers.First().VersionedInstanceIdentifier), - _recyclableMemoryStreamManager, - rows: 200, - columns: 200, - frames: 100); - - MemoryStream copyStream = _recyclableMemoryStreamManager.GetStream(); - await streamAndStoredFile.Value.CopyToAsync(copyStream); - copyStream.Position = 0; - streamAndStoredFile.Value.Position = 0; - - var binaryData = await BinaryData.FromStreamAsync(copyStream); - copyStream.Position = 0; - - _fileStore.GetFileAsync(fileIdentifier, Partition.Default, DefaultFileProperties, cancellationToken).Returns(streamAndStoredFile.Value); - _fileStore.GetFilePropertiesAsync(newFileIdentifier, Partition.Default, DefaultFileProperties, cancellationToken).Returns(DefaultFileProperties); - _metadataStore.GetInstanceMetadataAsync(fileIdentifier, cancellationToken).Returns(streamAndStoredFile.Key.Dataset); - _metadataStore.StoreInstanceMetadataAsync(streamAndStoredFile.Key.Dataset, newFileIdentifier, cancellationToken).Returns(Task.CompletedTask); - _fileStore.GetFileContentInRangeAsync(newFileIdentifier, Partition.Default, DefaultFileProperties, Arg.Any(), cancellationToken).Returns(binaryData); - _fileStore.UpdateFileBlockAsync(newFileIdentifier, Partition.Default, DefaultFileProperties, Arg.Any(), copyStream, cancellationToken).Returns(DefaultFileProperties); - - _fileStore.StoreFileInBlocksAsync( - newFileIdentifier, - Partition.Default, - Arg.Any(), - _config.StageBlockSizeInBytes, - Arg.Any>(), - cancellationToken) - .Returns(DefaultFileProperties); - - await _updateInstanceService.UpdateInstanceBlobAsync(versionedInstanceIdentifiers.First(), datasetToUpdate, Partition.Default, cancellationToken); - - streamAndStoredFile.Key.Dataset.Remove(DicomTag.PixelData); - var firstBlockLength = await DicomFileExtensions.GetByteLengthAsync(streamAndStoredFile.Key, new RecyclableMemoryStreamManager()); - await _fileStore.DidNotReceive().CopyFileAsync(fileIdentifier, newFileIdentifier, Partition.Default, DefaultFileProperties, cancellationToken); - await _fileStore.Received(1).GetFileAsync(fileIdentifier, Partition.Default, DefaultFileProperties, cancellationToken); - await _metadataStore.Received(1).GetInstanceMetadataAsync(fileIdentifier, cancellationToken); - await _metadataStore.Received(1).StoreInstanceMetadataAsync(streamAndStoredFile.Key.Dataset, newFileIdentifier, cancellationToken); - await _fileStore.Received(1).GetFileContentInRangeAsync(newFileIdentifier, Partition.Default, DefaultFileProperties, Arg.Is(x => x.Length == firstBlockLength), cancellationToken); - await _fileStore.Received(1).UpdateFileBlockAsync(newFileIdentifier, Partition.Default, DefaultFileProperties, Arg.Any(), Arg.Any(), cancellationToken); - await _fileStore.Received(1).StoreFileInBlocksAsync( - newFileIdentifier, - Partition.Default, - Arg.Any(), - _config.StageBlockSizeInBytes, - Arg.Any>(), - cancellationToken); - - streamAndStoredFile.Value.Dispose(); - copyStream.Dispose(); - } - - [Fact] - public async Task GivenValidInputWithExistingFile_WhenCallingUpdateBlobAsync_ShouldCallUpdateInstanceFileAsync_AndUpdateInstanceMetadataAsync() - { - long fileIdentifier = 456; - long newFileIdentifier = 789; - - List versionedInstanceIdentifiers = SetupInstanceIdentifiersList(version: fileIdentifier, instanceProperty: new InstanceProperties { OriginalVersion = 123, NewVersion = newFileIdentifier, FileProperties = DefaultFileProperties }); - var datasetToUpdate = new DicomDataset(); - var cancellationToken = CancellationToken.None; - - KeyValuePair streamAndStoredFile = await RetrieveHelpers.StreamAndStoredFileFromDataset( - RetrieveHelpers.GenerateDatasetsFromIdentifiers( - versionedInstanceIdentifiers.First().VersionedInstanceIdentifier), - _recyclableMemoryStreamManager, - rows: 200, - columns: 200, - frames: 100); - var firstBlockLength = await DicomFileExtensions.GetByteLengthAsync(streamAndStoredFile.Key, new RecyclableMemoryStreamManager()); - - MemoryStream copyStream = _recyclableMemoryStreamManager.GetStream(); - await streamAndStoredFile.Value.CopyToAsync(copyStream); - copyStream.Position = 0; - streamAndStoredFile.Value.Position = 0; - - var binaryData = await BinaryData.FromStreamAsync(copyStream); - copyStream.Position = 0; - - byte[] buffer = new byte[firstBlockLength]; - await copyStream.ReadAsync(buffer, 0, buffer.Length); - var stream = new MemoryStream(buffer); - - var firstBlock = new KeyValuePair(Convert.ToBase64String(Guid.NewGuid().ToByteArray()), stream.Length); - - _fileStore.CopyFileAsync(fileIdentifier, newFileIdentifier, Partition.Default, DefaultFileProperties, cancellationToken).Returns(Task.CompletedTask); - _fileStore.GetFilePropertiesAsync(newFileIdentifier, Partition.Default, null, cancellationToken).Returns(DefaultFileProperties); - _fileStore.GetFileAsync(fileIdentifier, Partition.Default, DefaultFileProperties, cancellationToken).Returns(streamAndStoredFile.Value); - _metadataStore.GetInstanceMetadataAsync(fileIdentifier, cancellationToken).Returns(streamAndStoredFile.Key.Dataset); - _metadataStore.StoreInstanceMetadataAsync(streamAndStoredFile.Key.Dataset, newFileIdentifier, cancellationToken).Returns(Task.CompletedTask); - _fileStore.GetFileContentInRangeAsync(newFileIdentifier, Partition.Default, DefaultFileProperties, Arg.Any(), cancellationToken).Returns(binaryData); - _fileStore.UpdateFileBlockAsync(newFileIdentifier, Partition.Default, DefaultFileProperties, Arg.Any(), copyStream, cancellationToken).Returns(DefaultFileProperties); - _fileStore.GetFirstBlockPropertyAsync(newFileIdentifier, Partition.Default, DefaultFileProperties, cancellationToken).Returns(firstBlock); - - _fileStore.StoreFileInBlocksAsync( - newFileIdentifier, - Partition.Default, - Arg.Any(), - _config.StageBlockSizeInBytes, - Arg.Any>(), - cancellationToken) - .Returns(DefaultFileProperties); - - await _updateInstanceService.UpdateInstanceBlobAsync(versionedInstanceIdentifiers.First(), datasetToUpdate, Partition.Default, cancellationToken); - - streamAndStoredFile.Key.Dataset.Remove(DicomTag.PixelData); - - await _fileStore.Received(1).CopyFileAsync(fileIdentifier, newFileIdentifier, Partition.Default, DefaultFileProperties, cancellationToken); - await _fileStore.Received(1).GetFilePropertiesAsync(newFileIdentifier, Partition.Default, null, cancellationToken); - await _fileStore.DidNotReceive().GetFileAsync(fileIdentifier, Partition.Default, DefaultFileProperties, cancellationToken); - await _metadataStore.Received(1).GetInstanceMetadataAsync(fileIdentifier, cancellationToken); - await _metadataStore.Received(1).StoreInstanceMetadataAsync(streamAndStoredFile.Key.Dataset, newFileIdentifier, cancellationToken); - await _fileStore.Received(1).GetFileContentInRangeAsync(newFileIdentifier, Partition.Default, DefaultFileProperties, Arg.Is(x => x.Length == firstBlockLength), cancellationToken); - await _fileStore.Received(1).UpdateFileBlockAsync(newFileIdentifier, Partition.Default, DefaultFileProperties, Arg.Any(), Arg.Any(), cancellationToken); - await _fileStore.DidNotReceive().StoreFileInBlocksAsync( - newFileIdentifier, - Partition.Default, - Arg.Any(), - _config.StageBlockSizeInBytes, - Arg.Any>(), - cancellationToken); - await _fileStore.Received(1).GetFirstBlockPropertyAsync(newFileIdentifier, Partition.Default, DefaultFileProperties, cancellationToken); - - streamAndStoredFile.Value.Dispose(); - copyStream.Dispose(); - } - - private static List SetupInstanceIdentifiersList(long version, Partition partition = null, InstanceProperties instanceProperty = null, long? newVersion = null) - { - var dicomInstanceIdentifiersList = new List(); - newVersion ??= version; - instanceProperty ??= new InstanceProperties { NewVersion = newVersion, FileProperties = DefaultFileProperties }; - partition ??= Partition.Default; - dicomInstanceIdentifiersList.Add(new InstanceMetadata(new VersionedInstanceIdentifier(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), version, partition), instanceProperty)); - return dicomInstanceIdentifiersList; - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Update/UpdateRequestValidatorTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Update/UpdateRequestValidatorTests.cs deleted file mode 100644 index 785e57c2a8..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Update/UpdateRequestValidatorTests.cs +++ /dev/null @@ -1,105 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Update; -using Microsoft.Health.Dicom.Core.Models.Update; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Update; - -public class UpdateRequestValidatorTests -{ - [Theory] - [MemberData(nameof(GetInvalidStudyInstanceUidsCount))] - public void GivenInvalidStudyInstanceUids_WhenValidated_ThenBadRequestExceptionShouldBeThrown(IReadOnlyList studyInstanceUids) - { - UpdateSpecification updateSpecification = new UpdateSpecification(studyInstanceUids, null); - Assert.Throws(() => UpdateRequestValidator.ValidateRequest(updateSpecification)); - } - - [Theory] - [MemberData(nameof(GetInvalidStudyInstanceUids))] - public void GivenInvalidStudyInstanceIds_WhenValidated_ThenInvalidIdentifierExceptionShouldBeThrown(IReadOnlyList studyInstanceUids) - { - UpdateSpecification updateSpecification = new UpdateSpecification(studyInstanceUids, null); - Assert.Throws(() => UpdateRequestValidator.ValidateRequest(updateSpecification)); - } - - [Theory] - [MemberData(nameof(GetValidStudyInstanceUids))] - public void GivenValidStudyInstanceIds_WhenValidated_ThenItShouldSucceed(IReadOnlyList studyInstanceUids) - { - UpdateSpecification updateSpecification = new UpdateSpecification(studyInstanceUids, null); - UpdateRequestValidator.ValidateRequest(updateSpecification); - } - - [Fact] - public void GivenNullDataset_WhenValidated_ThenArgumentNullExceptionShouldBeThrown() - { - Assert.Throws(() => UpdateRequestValidator.ValidateDicomDataset(null)); - } - - [Theory] - [MemberData(nameof(GetValidDicomDataset))] - public void GivenAValidDataset_WhenValidated_ThenItShouldSucceed(DicomDataset dataset) - { - UpdateRequestValidator.ValidateDicomDataset(dataset); - } - - [Theory] - [MemberData(nameof(GetInvalidDicomDataset))] - public void GivenAnInvalidDataset_WhenValidated_ThenSucceedsWithErrorsInFailedAttributesSequence(DicomDataset dataset) - { - string errorComment = "DICOM100: (0020,000d) - Updating the tag is not supported"; - DicomDataset failedSop = UpdateRequestValidator.ValidateDicomDataset(dataset); - DicomSequence failedAttributeSequence = failedSop.GetSequence(DicomTag.FailedAttributesSequence); - Assert.Single(failedAttributeSequence); - Assert.Equal(errorComment, failedAttributeSequence.Items[0].GetString(DicomTag.ErrorComment)); - } - - public static IEnumerable GetValidStudyInstanceUids() - { - yield return new object[] { new List() { "1.2.3.4" } }; - yield return new object[] { new List() { "1.2.3.4", "1.2.3.5" } }; - } - - public static IEnumerable GetInvalidStudyInstanceUids() - { - yield return new object[] { new List() { "1.a1.2" } }; - yield return new object[] { new List() { "invalid" } }; - } - - public static IEnumerable GetInvalidStudyInstanceUidsCount() - { - yield return new object[] { null }; - yield return new object[] { new List() }; - yield return new object[] { new List() { - "1.1.1.1", "1.1.1.2", "1.1.1.3", "1.1.1.4", "1.1.1.5", "1.1.1.6", "1.1.1.7", "1.1.1.8", "1.1.1.9", "1.1.1.10", - "1.1.2.1", "1.1.2.2", "1.1.2.3", "1.1.2.4", "1.1.2.5", "1.1.2.6", "1.1.2.7", "1.1.2.8", "1.1.2.9", "1.1.2.10", - "1.1.3.1", "1.1.3.2", "1.1.3.3", "1.1.3.4", "1.1.3.5", "1.1.3.6", "1.1.3.7", "1.1.3.8", "1.1.3.9", "1.1.3.10", - "1.1.4.1", "1.1.4.2", "1.1.4.3", "1.1.4.4", "1.1.4.5", "1.1.4.6", "1.1.4.7", "1.1.4.8", "1.1.4.9", "1.1.4.10", - "1.1.5.1", "1.1.5.2", "1.1.5.3", "1.1.5.4", "1.1.5.5", "1.1.5.6", "1.1.5.7", "1.1.5.8", "1.1.5.9", "1.1.5.10", - "1.1.6.1", "1.1.6.2", "1.1.6.3", "1.1.6.4", "1.1.6.5", "1.1.6.6", "1.1.6.7", "1.1.6.8", "1.1.6.9", "1.1.6.10" } }; - } - - public static IEnumerable GetValidDicomDataset() - { - yield return new object[] { new DicomDataset(new DicomPersonName(DicomTag.PatientBirthName, "foo")) }; - yield return new object[] { new DicomDataset() - { - { DicomTag.PatientID, "123" }, - { DicomTag.PatientName, "Anonymous" } - } }; - } - - public static IEnumerable GetInvalidDicomDataset() - { - yield return new object[] { new DicomDataset(new DicomShortString(DicomTag.StudyInstanceUID, "Issuer")) }; - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Validation/DateValidationTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Validation/DateValidationTests.cs deleted file mode 100644 index 245a7bc673..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Validation/DateValidationTests.cs +++ /dev/null @@ -1,45 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Validation; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Validation; - -public class DateValidationTests -{ - private readonly DateValidation _validation = new DateValidation(); - - [Theory] - [InlineData("20100141")] - [InlineData("233434343")] - public void GivenDAInvalidValue_WhenValidating_ThenShouldThrows(string value) - { - DicomDate element = new DicomDate(DicomTag.Date, value); - var ex = Assert.Throws(() => _validation.Validate(element)); - Assert.Equal(ValidationErrorCode.DateIsInvalid, ex.ErrorCode); - } - - [Theory] - [InlineData("20210313")] - [InlineData("20210313\0")] - [InlineData(null)] - [InlineData("")] - public void GivenDAValidateValue_WhenValidating_ThenShouldPass(string value) - { - DicomDate element = new DicomDate(DicomTag.Date, value); - _validation.Validate(element); - } - - [Fact] - public void GivenDAValidateMultipleValues_WhenValidating_ThenShouldValidateFirstOne() - { - // First one is valid, while second is invalid - DicomDate element = new DicomDate(DicomTag.Date, "20210313", "20100141"); - _validation.Validate(element); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Validation/DicomUidValidationTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Validation/DicomUidValidationTests.cs deleted file mode 100644 index 526be99b64..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Validation/DicomUidValidationTests.cs +++ /dev/null @@ -1,54 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Validation; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Validation; - -public class DicomUidValidationTests -{ - - [Theory] - [InlineData("13.14.520")] - [InlineData("13.14.520\0")] - [InlineData("13")] - public void GivenValidateUid_WhenValidating_ThenShouldPass(string value) - { - DicomElement element = new DicomUniqueIdentifier(DicomTag.DigitalSignatureUID, value); - new UidValidation().Validate(element); - } - - [Theory] - [InlineData("")] - [InlineData("\0")] - [InlineData(null)] - public void GivenValidateUid_WhenValidatingNullOrEmpty_ThenShouldNotPass(string value) - { - DicomElement element = new DicomUniqueIdentifier(DicomTag.DigitalSignatureUID, value); - new UidValidation().Validate(element); - } - - [Fact] - public void GivenMultipleValues_WhenValidating_ThenShouldVaidateFirstOne() - { - DicomElement element = new DicomUniqueIdentifier(DicomTag.DigitalSignatureUID, "13", "11|"); - new UidValidation().Validate(element); - } - - [Theory] - [InlineData("123.")] // end with . - [InlineData("abc.123")] // a is invalid character - [InlineData("11|")] // | is invalid character - [InlineData("0123456789012345678901234567890123456789012345678901234567890123456789")] // value is too long - public void GivenInvalidUidWhenValidating_ThenShouldThrow(string value) - { - DicomElement element = new DicomUniqueIdentifier(DicomTag.DigitalSignatureUID, value); - Assert.Throws(() => new UidValidation().Validate(element)); - } - -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Validation/ElementMaxLengthValidationTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Validation/ElementMaxLengthValidationTests.cs deleted file mode 100644 index 2711803cf8..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Validation/ElementMaxLengthValidationTests.cs +++ /dev/null @@ -1,49 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Validation; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Validation; - -public class ElementMaxLengthValidationTests -{ - - [Fact] - public void GivenValueExceedMaxLength_WhenValidating_ThenShouldThrows() - { - var ex = Assert.Throws(() => - new ElementMaxLengthValidation(12).Validate(new DicomIntegerString(DicomTag.DoseReferenceNumber, "0123456789121"))); - Assert.Equal(ValidationErrorCode.ExceedMaxLength, ex.ErrorCode); - } - - [Theory] - [InlineData("012345678912")] - [InlineData("")] - [InlineData("\0")] - [InlineData(null)] - public void GivenValueInRange_WhenValidating_ThenShouldPass(string value) - { - new ElementMaxLengthValidation(12).Validate(new DicomIntegerString(DicomTag.DoseReferenceNumber, value)); - } - - [Fact] - public void GivenMultipleValues_WhenValidating_ThenShouldValidateFirstOne() - { - // First one in range, second one out of range. - new ElementMaxLengthValidation(12).Validate(new DicomIntegerString(DicomTag.DoseReferenceNumber, "012345678912", "0123456789121")); - } - - [Theory] - [InlineData("")] - [InlineData(null)] - public void GivenValidate_WhenValidatingNullOrEmpty_ThenShouldPass(string value) - { - DicomElement element = new DicomIntegerString(DicomTag.DoseReferenceNumber, value); - new ElementMaxLengthValidation(4).Validate(element); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Validation/ElementMinimumValidatorTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Validation/ElementMinimumValidatorTests.cs deleted file mode 100644 index 4386c071e4..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Validation/ElementMinimumValidatorTests.cs +++ /dev/null @@ -1,49 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Globalization; -using FellowOakDicom; -using FellowOakDicom.IO; -using Microsoft.Health.Dicom.Core.Features.Validation; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Validation; - -public class ElementMinimumValidatorTests -{ - private readonly IElementMinimumValidator _validator = new ElementMinimumValidator(); - - [Theory] - [MemberData(nameof(SupportedDicomElements))] - public void GivenSupportedVR_WhenValidating_ThenShouldPass(DicomElement dicomElement) - { - _validator.Validate(dicomElement); - } - - public static IEnumerable SupportedDicomElements() - { - yield return new object[] { new DicomApplicationEntity(DicomTag.DestinationAE, "012") }; - yield return new object[] { new DicomAgeString(DicomTag.PatientAge, "012W") }; - - yield return new object[] { new DicomCodeString(DicomTag.AcquisitionStartCondition, "0123456789 ") }; - yield return new object[] { new DicomDate(DicomTag.AcquisitionDate, "20210313") }; - - yield return new object[] { new DicomFloatingPointSingle(DicomTag.AnchorPoint, ByteConverter.ToByteBuffer(new float[] { float.MaxValue })) }; - yield return new object[] { new DicomFloatingPointDouble(DicomTag.DopplerCorrectionAngle, ByteConverter.ToByteBuffer(new double[] { double.MaxValue })) }; - - yield return new object[] { new DicomIntegerString(DicomTag.DoseReferenceNumber, int.MaxValue.ToString("D12", CultureInfo.InvariantCulture)) }; - yield return new object[] { new DicomLongString(DicomTag.WindowCenterWidthExplanation, "0123456789012345678901234567890123456789012345678901234567891234") }; - yield return new object[] { new DicomPersonName(DicomTag.PatientName, "abc^xyz=abc^xyz^xyz^xyz^xyz=abc^xyz") }; - - yield return new object[] { new DicomShortString(DicomTag.AccessionNumber, "0123456789123456") }; - yield return new object[] { new DicomSignedLong(DicomTag.DisplayedAreaBottomRightHandCorner, int.MaxValue) }; - yield return new object[] { new DicomSignedShort(DicomTag.LargestImagePixelValue, short.MaxValue) }; - - yield return new object[] { new DicomUniqueIdentifier(DicomTag.DigitalSignatureUID, "13.14.520") }; - yield return new object[] { new DicomUnsignedLong(DicomTag.DopplerSampleVolumeXPositionRetiredRETIRED, uint.MaxValue) }; - yield return new object[] { new DicomUnsignedShort(DicomTag.AcquisitionMatrix, ushort.MaxValue) }; - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Validation/ElementRequiredLengthValidationTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Validation/ElementRequiredLengthValidationTests.cs deleted file mode 100644 index 4bc4ce2534..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Validation/ElementRequiredLengthValidationTests.cs +++ /dev/null @@ -1,82 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using FellowOakDicom; -using FellowOakDicom.IO; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Validation; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Validation; - -public class ElementRequiredLengthValidationTests -{ - - [Fact] - public void GivenBinaryValueNotRequiredLength_WhenValidating_ThenShouldThrows() - { - DicomElement element = new DicomSignedShort(DicomTag.LargestImagePixelValue, ByteConverter.ToByteBuffer(new byte[] { byte.MaxValue })); - var ex = Assert.Throws(() => new ElementRequiredLengthValidation(4).Validate(element)); - Assert.Equal(ValidationErrorCode.UnexpectedLength, ex.ErrorCode); - } - - [Fact] - public void GivenEmptyBinaryValue_WhenValidating_ThenShouldThrows() - { - DicomElement element = new DicomSignedShort(DicomTag.LargestImagePixelValue, ByteConverter.ToByteBuffer(new byte[0])); - var ex = Assert.Throws(() => new ElementRequiredLengthValidation(4).Validate(element)); - Assert.Equal(ValidationErrorCode.UnexpectedLength, ex.ErrorCode); - } - - [Fact] - public void GivenBinaryValueOfRequiredLength_WhenValidating_ThenShouldPass() - { - DicomElement element = new DicomSignedShort(DicomTag.LargestImagePixelValue, short.MaxValue); - new ElementRequiredLengthValidation(2).Validate(element); - } - - [Fact] - public void GivenMultipleBinaryValues_WhenValidating_ThenShouldValidateFirstOne() - { - // First value if valid, second value is invalid - DicomElement element = new DicomSignedShort(DicomTag.LargestImagePixelValue, ByteConverter.ToByteBuffer(new byte[] { 1, 2, 3 })); - new ElementRequiredLengthValidation(2).Validate(element); - } - - [Fact] - public void GivenStringValueNotRequiredLength_WhenValidating_ThenShouldThrows() - { - DicomElement element = new DicomAgeString(DicomTag.PatientAge, "012W1"); - var ex = Assert.Throws(() => new ElementRequiredLengthValidation(4).Validate(element)); - Assert.Equal(ValidationErrorCode.UnexpectedLength, ex.ErrorCode); - } - - [Fact] - public void GivenStringValueOfRequiredLength_WhenValidating_ThenShouldPass() - { - DicomElement element = new DicomAgeString(DicomTag.PatientAge, "012W"); - new ElementRequiredLengthValidation(4).Validate(element); - } - - [Fact] - public void GivenMultipleStringValues_WhenValidating_ThenShouldValidateFirstOne() - { - // First is valid, second is invalid - DicomElement element = new DicomAgeString(DicomTag.PatientAge, "012W", "012W2"); - new ElementRequiredLengthValidation(4).Validate(element); - } - - [Theory] - [InlineData("", 0)] - [InlineData("", 1)] - [InlineData(null, 0)] - [InlineData(null, 1)] - [InlineData("123\0", 4)] - public void GivenValidate_WhenValidatingNullOrEmpty_ThenShouldNotPass(string value, int requiredLength) - { - DicomElement element = new DicomAgeString(DicomTag.PatientAge, value); - Assert.Throws(() => new ElementRequiredLengthValidation(requiredLength).Validate(element)); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Validation/EncodedStringElementValidationTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Validation/EncodedStringElementValidationTests.cs deleted file mode 100644 index ac81acf8ed..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Validation/EncodedStringElementValidationTests.cs +++ /dev/null @@ -1,68 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Globalization; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Validation; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Validation; - -public class EncodedStringElementValidationTests -{ - private readonly EncodedStringElementValidation _validation = new(); - - [Theory] - [InlineData("")] - [InlineData(null)] - [InlineData("\0")] - public void GivenValidate_WhenValidatingNullOrEmpty_ThenShouldPass(string value) - { - DicomElement element = new DicomTime(DicomTag.Time, value); - _validation.Validate(element); - } - - [Theory] - [MemberData(nameof(ValidElements))] - public void GivenValidDicomStringElement_WhenValidating_ThenPass(DicomElement element) - => _validation.Validate(element); - - [Theory] - [MemberData(nameof(InvalidElements))] - public void GivenInvalidDicomStringElement_WhenValidating_ThenThrowElementValidationException(DicomElement element, ValidationErrorCode expectedError) - { - ElementValidationException exception = Assert.Throws(() => _validation.Validate(element)); - Assert.Equal(expectedError, exception.ErrorCode); - Assert.Contains(expectedError.GetMessage(), exception.Message); - } - - - [Fact] - public void GivenDicomStringElementWithMultipleValues_WhenValidating_ThenShouldValidateFirstOne() - { - var element = new DicomTime(DicomTag.Time, DateTime.UtcNow.ToString("HHmmss'.'fffff", CultureInfo.InvariantCulture), "ABC"); - _validation.Validate(element); - } - - public static IEnumerable ValidElements = new object[][] - { - new object[] { new DicomDateTime(DicomTag.EffectiveDateTime, DateTimeOffset.UtcNow.ToString("yyyyMMddHHmmss'.'ffffff'+'0000", CultureInfo.InvariantCulture)) }, - new object[] { new DicomIntegerString(DicomTag.PixelAspectRatio, "0012345") }, - new object[] { new DicomTime(DicomTag.Time, DateTime.UtcNow.ToString("HHmmss'.'fffff", CultureInfo.InvariantCulture)) }, - new object[] { new DicomTime(DicomTag.Time, (string)null )}, - }; - - public static object[][] InvalidElements = new object[][] - { - new object[] { new DicomDateTime(DicomTag.EffectiveDateTime, "6"), ValidationErrorCode.DateTimeIsInvalid }, - new object[] { new DicomIntegerString(DicomTag.PixelAspectRatio, "1234567890123"), ValidationErrorCode.IntegerStringIsInvalid }, - new object[] { new DicomIntegerString(DicomTag.PixelAspectRatio, "twelve"), ValidationErrorCode.IntegerStringIsInvalid }, - new object[] { new DicomTime(DicomTag.Time, "ABC"), ValidationErrorCode.TimeIsInvalid }, - }; -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Validation/FoDicomValidationTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Validation/FoDicomValidationTests.cs deleted file mode 100644 index e79f8ee3b9..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Validation/FoDicomValidationTests.cs +++ /dev/null @@ -1,29 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using FellowOakDicom; -using Microsoft.Health.Dicom.Tests.Common; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Validation; - -/// -/// This is a temporary test to show the difference in validation based on how the method is called -/// -public class FoDicomValidationTests -{ - [Fact] - public void WhenValidatingDirectly_ExpectNullsAccepted_AndOtherwiseNullRefThrown() - { - string nullValue = null; - DicomDataset dicomDataset = Samples.CreateRandomInstanceDataset(validateItems: false); - dicomDataset.Add(DicomTag.AcquisitionDateTime, nullValue); - - DicomElement de = dicomDataset.GetDicomItem(DicomTag.AcquisitionDateTime); - de.Validate(); - Assert.Throws(() => de.ValueRepresentation.ValidateString(nullValue)); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Validation/ImplicitValueRepresentationValidatorTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Validation/ImplicitValueRepresentationValidatorTests.cs deleted file mode 100644 index a599714d44..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Validation/ImplicitValueRepresentationValidatorTests.cs +++ /dev/null @@ -1,59 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Features.Validation; -using Microsoft.Health.Dicom.Tests.Common; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Validation; - -public class ImplicitValueRepresentationValidatorTests -{ - [Theory] - [MemberData(nameof(GetExplicitVRTransferSyntax))] - public void GivenDicomDatasetWithNonImplicitVR_WhenValidating_ReturnsTrue(DicomTransferSyntax transferSyntax) - { - var dicomDataset = Samples - .CreateRandomInstanceDataset(dicomTransferSyntax: transferSyntax) - .NotValidated(); - - Assert.False(ImplicitValueRepresentationValidator.IsImplicitVR(dicomDataset)); - } - - [Theory] - [MemberData(nameof(GetNonExplicitVRTransferSyntax))] - public void GivenDicomDatasetWithImplicitVR_WhenValidating_ReturnsFalse(DicomTransferSyntax transferSyntax) - { - var dicomDataset = Samples - .CreateRandomInstanceDataset(dicomTransferSyntax: transferSyntax) - .NotValidated(); - - Assert.True(ImplicitValueRepresentationValidator.IsImplicitVR(dicomDataset)); - } - - public static IEnumerable GetExplicitVRTransferSyntax() - { - foreach (var ts in Samples.GetAllDicomTransferSyntax()) - { - if (!ts.IsExplicitVR) - continue; - - yield return new object[] { ts }; - } - } - - public static IEnumerable GetNonExplicitVRTransferSyntax() - { - foreach (var ts in Samples.GetAllDicomTransferSyntax()) - { - if (ts.IsExplicitVR) - continue; - - yield return new object[] { ts }; - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Validation/LongStringValidationTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Validation/LongStringValidationTests.cs deleted file mode 100644 index b5aa5de4a6..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Validation/LongStringValidationTests.cs +++ /dev/null @@ -1,45 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Validation; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Validation; - -public class LongStringValidationTests -{ - - [Theory] - [InlineData("")] - [InlineData("012345678912")] - [InlineData("012345678912\0")] - [InlineData("\0")] - [InlineData(null)] - public void GivenValidate_WhenValidatingNullOrEmpty_ThenShouldPass(string value) - { - DicomElement element = new DicomLongString(DicomTag.WindowCenterWidthExplanation, value); - new LongStringValidation().Validate(element); - } - - [Fact] - public void GivenMultipleValues_WhenValidating_ThenShouldValidateFirstOne() - { - var element = new DicomLongString(DicomTag.WindowCenterWidthExplanation, "012345678912", "0123456789012345678901234567890123456789012345678901234567890123456789"); - new LongStringValidation().Validate(element); - } - - [Theory] - [InlineData("0123456789012345678901234567890123456789012345678901234567890123456789", ValidationErrorCode.ExceedMaxLength)] // exceed max length - [InlineData("012\n", ValidationErrorCode.InvalidCharacters)] // contains control character except Esc - public void GivenInvalidLongString_WhenValidating_ThenShouldThrow(string value, ValidationErrorCode errorCode) - { - DicomElement element = new DicomLongString(DicomTag.WindowCenterWidthExplanation, value); - var ex = Assert.Throws(() => new LongStringValidation().Validate(element)); - Assert.Equal(ex.ErrorCode, errorCode); - } - -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Validation/PartitionNameValidatorTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Validation/PartitionNameValidatorTests.cs deleted file mode 100644 index 98762caa48..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Validation/PartitionNameValidatorTests.cs +++ /dev/null @@ -1,39 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Validation; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Validation; - -public class PartitionNameValidatorTests -{ - [Theory] - [InlineData("fooBAR")] - [InlineData("fooBAR123")] - [InlineData("fooBAR123.-_")] - [InlineData("62f5c7eb-124a-49b1-9e5c-17c81a1a7137")] - [InlineData("62f5c7eb_124a_49b1_9e5c-17c81a1a7137")] - [InlineData("62f5c7eb.124a.49b1.9e5c.17c81a1a7137")] - [InlineData("13.14.520")] - [InlineData("1")] - public void GivenValidPartitionId_WhenValidating_ThenShouldPass(string value) - { - PartitionNameValidator.Validate(value); - } - - [Theory] - [InlineData("")] // empty string - [InlineData("123 ")] // has a space - [InlineData("abc123@!")] // @ & ! are invalid characters - [InlineData("62f5c7eb-124a-49b1-9e5c-17c81a1a7137/")] // / is invalid character - [InlineData("0123456789012345678901234567890123456789012345678901234567890123456789")] // value is too long - public void GivenInValidPartitionId_WhenValidating_ThenShoulFail(string value) - { - Assert.Throws(() => PartitionNameValidator.Validate(value)); - } - -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Validation/PersonNameValidationTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Validation/PersonNameValidationTests.cs deleted file mode 100644 index 71e933a4fe..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Validation/PersonNameValidationTests.cs +++ /dev/null @@ -1,45 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Validation; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Validation; - -public class PersonNameValidationTests -{ - [Theory] - [InlineData("abc^xyz=abc^xyz^xyz^xyz^xyz=abc^xyz")] - [InlineData("abc^xyz=abc^xyz^xyz^xyz^xyz=abc^xyz\0")] - [InlineData("")] - [InlineData(null)] - public void GivenValidate_WhenValidatingNullOrEmpty_ThenShouldPass(string value) - { - DicomElement element = new DicomPersonName(DicomTag.PatientName, value); - new PersonNameValidation().Validate(element); - } - - [Fact] - public void GivenMultipleValues_WhenValidating_ThenShouldValidateFirstOne() - { - DicomElement element = new DicomPersonName(DicomTag.PatientName, new string[] { "abc^xyz=abc^xyz^xyz^xyz^xyz=abc^xyz", "abc^efg^hij^pqr^lmn^xyz" }); - new PersonNameValidation().Validate(element); - } - - [Theory] - [InlineData("abc^xyz=abc^xyz=abc^xyz=abc^xyz", ValidationErrorCode.PersonNameExceedMaxGroups)] // too many groups (>3) - [InlineData("abc^efg^hij^pqr^lmn^xyz", ValidationErrorCode.PersonNameExceedMaxComponents)] // to many group components - [InlineData("0123456789012345678901234567890123456789012345678901234567890123456789", ValidationErrorCode.PersonNameGroupExceedMaxLength)] // group is too long - public void GivenInvalidPatientName_WhenValidating_ThenShouldThrow(string value, ValidationErrorCode errorCode) - { - DicomElement element = new DicomPersonName(DicomTag.PatientName, value); - var ex = Assert.Throws(() => new PersonNameValidation().Validate(element)); - Assert.Equal(errorCode, ex.ErrorCode); - - } - -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Validation/ReindexDatasetValidatorTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Validation/ReindexDatasetValidatorTests.cs deleted file mode 100644 index df0d555913..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Validation/ReindexDatasetValidatorTests.cs +++ /dev/null @@ -1,83 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Features.Indexing; -using Microsoft.Health.Dicom.Core.Features.Validation; -using Microsoft.Health.Dicom.Tests.Common; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Validation; - -public class ReindexDatasetValidatorTests -{ - private readonly IReindexDatasetValidator _datasetValidator; - private readonly IElementMinimumValidator _validator = new ElementMinimumValidator(); - private readonly IExtendedQueryTagErrorsService _tagErrorsService; - - public ReindexDatasetValidatorTests() - { - _tagErrorsService = Substitute.For(); - _datasetValidator = new ReindexDatasetValidator(_validator, _tagErrorsService); - - DicomValidationBuilderExtension.SkipValidation(null); - } - - [Fact] - public async Task GivenValidAndInvalidTagValues_WhenValidate_ThenReturnedValidTagsAndStoredFailure() - { - DicomTag tag1 = DicomTag.AcquisitionDateTime; - DicomTag tag2 = DicomTag.DeviceID; - DicomElement element1 = new DicomDateTime(tag1, "testvalue1"); - DicomElement element2 = new DicomLongString(tag2, "testvalue2"); - - DicomDataset ds = Samples.CreateRandomInstanceDataset(); - ds.Add(element1); - ds.Add(element2); - - var queryTag1 = new QueryTag(new ExtendedQueryTagStoreEntry(1, tag1.GetPath(), element1.ValueRepresentation.Code, null, QueryTagLevel.Instance, ExtendedQueryTagStatus.Ready, QueryStatus.Enabled, 0)); - var queryTag2 = new QueryTag(new ExtendedQueryTagStoreEntry(2, tag2.GetPath(), element2.ValueRepresentation.Code, null, QueryTagLevel.Instance, ExtendedQueryTagStatus.Ready, QueryStatus.Enabled, 0)); - - using var source = new CancellationTokenSource(); - - // only return querytag2 - IReadOnlyCollection validQueryTags = await _datasetValidator.ValidateAsync(ds, 1, new[] { queryTag1, queryTag2 }, source.Token); - Assert.Same(queryTag2, validQueryTags.Single()); - - // error for querytag1 is logged - await _tagErrorsService - .Received(1) - .AddExtendedQueryTagErrorAsync(queryTag1.ExtendedQueryTagStoreEntry.Key, ValidationErrorCode.DateTimeIsInvalid, 1, source.Token); - } - - [Fact] - public async Task GivenANullValue_WhenValidate_ThenReturnStoredFailure() - { - string nullValue = null; - DicomTag tag1 = DicomTag.AcquisitionDateTime; - DicomElement element1 = new DicomDateTime(tag1, nullValue); - - DicomDataset ds = Samples.CreateRandomInstanceDataset(); -#pragma warning disable CS0618 - ds.AutoValidate = false; -#pragma warning restore CS0618 - ds.Add(element1); - - var queryTag1 = new QueryTag(new ExtendedQueryTagStoreEntry(1, tag1.GetPath(), element1.ValueRepresentation.Code, null, QueryTagLevel.Instance, ExtendedQueryTagStatus.Ready, QueryStatus.Enabled, 0)); - - using var source = new CancellationTokenSource(); - - IReadOnlyCollection validQueryTags = await _datasetValidator.ValidateAsync(ds, 1, new[] { queryTag1 }, source.Token); - - Assert.Same(queryTag1, validQueryTags.Single()); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Validation/StringElementValidationTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Validation/StringElementValidationTests.cs deleted file mode 100644 index f8fbcd7c33..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Validation/StringElementValidationTests.cs +++ /dev/null @@ -1,109 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using FellowOakDicom; -using FellowOakDicom.IO.Buffer; -using Microsoft.Health.Dicom.Core.Features.Validation; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Validation; - -public class StringElementValidationTests -{ - private class StringValidation : StringElementValidation - { - protected override void ValidateStringElement(string name, DicomVR vr, string value, IByteBuffer buffer) - { - if (value.Contains('\0')) - { - throw new Exception(value); - } - } - } - - private class StringValidationNotAllowedNulls : StringElementValidation - { - protected override bool AllowNullOrEmpty => false; - - protected override void ValidateStringElement(string name, DicomVR vr, string value, IByteBuffer buffer) - { - if (string.IsNullOrEmpty(value) || value.Contains('\0')) - { - throw new Exception(value); - } - } - } - - [Theory] - [InlineData("13.14.520")] - [InlineData("13")] - [InlineData("13\0\0\0")] - [InlineData("\0\0\0")] - [InlineData(null)] - public void GivenAValue_WhenValidatingWithLeniencyAndAllowableNullOrEmpty_ThenShouldPass(string value) - { - DicomElement element = new DicomUniqueIdentifier(DicomTag.DigitalSignatureUID, value); - new StringValidation().Validate(element); - } - - [Theory] - [InlineData("13.14.520")] - [InlineData("13")] - [InlineData(null)] - public void GivenAValue_WhenValidatingWithoutLeniencyAndAllowableNullOrEmpty_ThenShouldPass(string value) - { - DicomElement element = new DicomUniqueIdentifier(DicomTag.DigitalSignatureUID, value); - new StringValidation().Validate(element, ValidationLevel.Strict); - } - - [Theory] - [InlineData("13\0\0\0")] - [InlineData("\0\0\0")] - public void GivenAValue_WhenValidatingWithoutLeniencyAndWithNullPadding_ThenShouldNotPass(string value) - { - DicomElement element = new DicomUniqueIdentifier(DicomTag.DigitalSignatureUID, value); - Assert.Throws(() => new StringValidation().Validate(element, ValidationLevel.Strict)); - } - - [Theory] - [InlineData("13.14.520")] - [InlineData("13")] - [InlineData("13\0\0\0")] - public void GivenAValue_WhenValidatingWithLeniencyAndNullOrEmptyNotAllowed_ThenShouldPass(string value) - { - DicomElement element = new DicomUniqueIdentifier(DicomTag.DigitalSignatureUID, value); - new StringValidationNotAllowedNulls().Validate(element); - } - - [Theory] - [InlineData("\0\0\0")] - [InlineData(null)] - public void GivenAValue_WhenValidatingWithLeniencyAndNullOrEmptyNotAllowed_ThenShouldNotPass(string value) - { - DicomElement element = new DicomUniqueIdentifier(DicomTag.DigitalSignatureUID, value); - Assert.Throws(() => new StringValidationNotAllowedNulls().Validate(element)); - } - - [Theory] - [InlineData("13.14.520")] - [InlineData("13")] - public void GivenAValue_WhenValidatingWithoutLeniencyAndNullOrEmptyNotAllowed_ThenShouldPass(string value) - { - DicomElement element = new DicomUniqueIdentifier(DicomTag.DigitalSignatureUID, value); - new StringValidationNotAllowedNulls().Validate(element, ValidationLevel.Strict); - } - - [Theory] - [InlineData("13\0\0\0")] - [InlineData("\0\0\0")] - [InlineData(null)] - public void GivenAValue_WhenValidatingWithoutLeniencyAndNullOrEmptyNotAllowed_ThenShouldNotPass(string value) - { - DicomElement element = new DicomUniqueIdentifier(DicomTag.DigitalSignatureUID, value); - Assert.Throws(() => new StringValidationNotAllowedNulls().Validate(element, ValidationLevel.Strict)); - } - -} \ No newline at end of file diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Validation/UidValidationTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Validation/UidValidationTests.cs deleted file mode 100644 index 98e05e7235..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Validation/UidValidationTests.cs +++ /dev/null @@ -1,42 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Microsoft.Health.Dicom.Core.Features.Validation; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Validation; - -public class UidValidationTests -{ - [Theory] - [InlineData(null)] - [InlineData("")] - public void GivenNullOrEmpty_WhenValidatingUidThatAllowsEmpty_ThenShouldPass(string value) - => Assert.True(UidValidation.IsValid(value, allowEmpty: true)); - - [Theory] - [InlineData("0")] - [InlineData("0.2.4.6.8")] - [InlineData("98.0.705.456.1.52365")] - [InlineData("123.0.45.6345.16765.0")] - [InlineData("12.0.0.678.324.145.123106.141.4905702.123480.9500026724.0.1.4020")] - [InlineData("12.0.0.678.324.145.123106.141.4905702.123480.9500026724.0.1.4020 ")] - [InlineData("007")] - [InlineData("12.003.456")] - public void GivenValidString_WhenValidatingAsUid_ThenShouldPass(string value) - => Assert.True(UidValidation.IsValid(value)); - - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData("hello.world")] - [InlineData("987.in.valid.654")] - [InlineData("24.foo.bar.baz")] - [InlineData("98-0-705-456-1-52365")] - [InlineData("123-0-45-6345-16765-0")] - [InlineData("12.0.0.678.324.145.123106.141.4905702.123480.9500026724.0.1.40201")] - public void GivenInvalidString_WhenValidatingAsUid_ThenShouldFail(string value) - => Assert.False(UidValidation.IsValid(value)); -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Workitem/AddWorkitemDatasetValidatorTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Workitem/AddWorkitemDatasetValidatorTests.cs deleted file mode 100644 index 4aa97f1bf1..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Workitem/AddWorkitemDatasetValidatorTests.cs +++ /dev/null @@ -1,56 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Features.Store; -using Microsoft.Health.Dicom.Core.Features.Workitem; -using Microsoft.Health.Dicom.Tests.Common; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Workitem; - -public class AddWorkitemDatasetValidatorTests -{ - [Fact] - public void GivenMissingRequiredTag_Throws() - { - var dataset = Samples.CreateRandomWorkitemInstanceDataset(); - var validator = new AddWorkitemDatasetValidator(); - - dataset = dataset.Remove(DicomTag.TransactionUID); - - Assert.Throws(() => validator.Validate(dataset)); - } - - [Fact] - public void GivenNotAllowedTag_WhenPresent_Throws() - { - var dataset = Samples.CreateRandomWorkitemInstanceDataset(); - - dataset = dataset.AddOrUpdate( - new DicomSequence( - DicomTag.ProcedureStepProgressInformationSequence, - new DicomDataset - { - { DicomTag.ProcedureStepProgress, "1.0" }, - })); - - var validator = new AddWorkitemDatasetValidator(); - - Assert.Throws(() => validator.Validate(dataset)); - } - - [Fact] - public void GivenTagShouldBeEmpty_WhenHasValue_Throws() - { - var dataset = Samples.CreateRandomWorkitemInstanceDataset(); - - dataset = dataset.AddOrUpdate(new DicomUniqueIdentifier(DicomTag.TransactionUID, "123")); - - var validator = new AddWorkitemDatasetValidator(); - - Assert.Throws(() => validator.Validate(dataset)); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Workitem/AddWorkitemRequestHandlerTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Workitem/AddWorkitemRequestHandlerTests.cs deleted file mode 100644 index 1add96f5bb..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Workitem/AddWorkitemRequestHandlerTests.cs +++ /dev/null @@ -1,43 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Threading; -using System.Threading.Tasks; -using FellowOakDicom; -using Microsoft.Health.Core.Features.Security.Authorization; -using Microsoft.Health.Dicom.Core.Features.Security; -using Microsoft.Health.Dicom.Core.Features.Workitem; -using Microsoft.Health.Dicom.Core.Messages.Workitem; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Workitem; - -public sealed class AddWorkitemRequestHandlerTests -{ - private readonly IWorkitemService _workitemService = Substitute.For(); - private readonly AddWorkitemRequestHandler _target; - - public AddWorkitemRequestHandlerTests() - { - _target = new AddWorkitemRequestHandler(new DisabledAuthorizationService(), _workitemService); - } - - [Fact] - public async Task GivenSupportedContentType_WhenHandled_ThenCorrectStoreResponseShouldBeReturned() - { - var workitemInstanceUid = string.Empty; - var request = new AddWorkitemRequest(new DicomDataset(), @"application/json", workitemInstanceUid); - - var response = new AddWorkitemResponse(WorkitemResponseStatus.Success, new Uri(@"https://www.microsoft.com")); - - _workitemService - .ProcessAddAsync(Arg.Any(), workitemInstanceUid, CancellationToken.None) - .Returns(response); - - Assert.Same(response, await _target.Handle(request, CancellationToken.None)); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Workitem/AddWorkitemResponseBuilderTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Workitem/AddWorkitemResponseBuilderTests.cs deleted file mode 100644 index 15d7d95507..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Workitem/AddWorkitemResponseBuilderTests.cs +++ /dev/null @@ -1,79 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Features.Store; -using Microsoft.Health.Dicom.Core.Features.Workitem; -using Microsoft.Health.Dicom.Core.Messages.Workitem; -using Microsoft.Health.Dicom.Tests.Common; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Workitem; - -public sealed class AddWorkitemResponseBuilderTests -{ - private readonly MockUrlResolver _urlResolver = new MockUrlResolver(); - private readonly DicomDataset _dataset = new DicomDataset(); - private readonly WorkitemResponseBuilder _target; - - public AddWorkitemResponseBuilderTests() - { - _target = new WorkitemResponseBuilder(_urlResolver); - } - - [Fact] - public void GivenBuildResponse_WhenNoFailure_ThenResponseStatusIsSuccess() - { - _dataset.Add(DicomTag.SOPInstanceUID, DicomUID.Generate().UID); - - _target.AddSuccess(_dataset); - - var response = _target.BuildAddResponse(); - - Assert.NotNull(response); - Assert.Equal(WorkitemResponseStatus.Success, response.Status); - } - - [Fact] - public void GivenBuildResponse_WhenNoFailure_ThenResponseUrlIncludesWorkitemInstanceUid() - { - var workitemInstanceUid = DicomUID.Generate().UID; - _dataset.Add(DicomTag.SOPInstanceUID, workitemInstanceUid); - - _target.AddSuccess(_dataset); - - var response = _target.BuildAddResponse(); - - Assert.NotNull(response); - Assert.NotNull(response.Uri); - Assert.Contains(workitemInstanceUid, response.Uri.ToString()); - } - - [Fact] - public void GivenBuildResponse_WhenFailure_ThenFailureReasonTagIsAddedToDicomDataset() - { - _target.AddFailure((ushort)WorkitemResponseStatus.Failure, dicomDataset: _dataset); - - var response = _target.BuildAddResponse(); - - Assert.NotNull(response); - Assert.NotEmpty(_dataset.GetString(DicomTag.FailureReason)); - Assert.Null(response.Uri); - Assert.Equal(WorkitemResponseStatus.Failure, response.Status); - } - - [Fact] - public void GivenBuildResponse_WhenConflict_ThenFailureReasonTagIsAddedToDicomDataset() - { - _target.AddFailure(FailureReasonCodes.SopInstanceAlreadyExists, dicomDataset: _dataset); - - var response = _target.BuildAddResponse(); - - Assert.NotNull(response); - Assert.NotEmpty(_dataset.GetString(DicomTag.FailureReason)); - Assert.Null(response.Uri); - Assert.Equal(WorkitemResponseStatus.Conflict, response.Status); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Workitem/CancelWorkitemDatasetValidatorTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Workitem/CancelWorkitemDatasetValidatorTests.cs deleted file mode 100644 index a01963f7c0..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Workitem/CancelWorkitemDatasetValidatorTests.cs +++ /dev/null @@ -1,86 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Features.Store; -using Microsoft.Health.Dicom.Core.Features.Workitem; -using Microsoft.Health.Dicom.Tests.Common; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Workitem; - -public class CancelWorkitemDatasetValidatorTests -{ - [Fact] - public void GivenMissingRequiredTag_Throws() - { - var dataset = Samples.CreateCanceledWorkitemDataset(@"Unit Test Reason", ProcedureStepState.Canceled); - dataset.Remove(DicomTag.SOPInstanceUID); - - var target = new CancelWorkitemDatasetValidator(); - - Assert.Throws(() => target.Validate(dataset)); - } - - [Fact] - public void GivenMissingLevel1ConditionalRequiredTag_DoesNotThrow() - { - var dataset = Samples.CreateCanceledWorkitemDataset(@"Unit Test Reason", ProcedureStepState.Canceled); - - var sequence = dataset.GetSequence(DicomTag.UnifiedProcedureStepPerformedProcedureSequence); - sequence.Items[0].Remove(DicomTag.ActualHumanPerformersSequence); - - var target = new CancelWorkitemDatasetValidator(); - - target.Validate(dataset); - } - - [Fact] - public void GivenMissingLevel2ConditionalRequiredTag_DoesNotThrows() - { - var dataset = Samples.CreateCanceledWorkitemDataset(@"Unit Test Reason", ProcedureStepState.Canceled); - var level1Sequence = dataset.GetSequence(DicomTag.UnifiedProcedureStepPerformedProcedureSequence); - var level2Sequence = level1Sequence.Items[0].GetSequence(DicomTag.ActualHumanPerformersSequence); - - level2Sequence.Items[0].Remove(DicomTag.HumanPerformerName); - - var target = new CancelWorkitemDatasetValidator(); - - target.Validate(dataset); - } - - [Fact] - public void GivenMissingPartiallyRequiredTagWithCanceledProcedureStepState_DoesNotThrow() - { - var dataset = Samples.CreateCanceledWorkitemDataset(@"Unit Test Reason", ProcedureStepState.Canceled); - dataset.Remove(DicomTag.UnifiedProcedureStepPerformedProcedureSequence); - - var target = new CancelWorkitemDatasetValidator(); - - target.Validate(dataset); - } - - [Fact] - public void GivenMissingPartiallyRequiredTagWithCompletedProcedureStepState_Throws() - { - var dataset = Samples.CreateCanceledWorkitemDataset(@"Unit Test Reason", ProcedureStepState.Completed); - dataset.Remove(DicomTag.UnifiedProcedureStepPerformedProcedureSequence); - - var target = new CancelWorkitemDatasetValidator(); - - Assert.Throws(() => target.Validate(dataset)); - } - - [Fact] - public void GivenEmptyValueForOptionalTag_DoesNotThrow() - { - var dataset = Samples.CreateCanceledWorkitemDataset(@"Unit Test Reason", ProcedureStepState.Canceled); - dataset.AddOrUpdate(DicomTag.ProcedureStepLabel, string.Empty); - - var target = new CancelWorkitemDatasetValidator(); - - target.Validate(dataset); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Workitem/CancelWorkitemResponseBuilderTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Workitem/CancelWorkitemResponseBuilderTests.cs deleted file mode 100644 index 7868fe2b27..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Workitem/CancelWorkitemResponseBuilderTests.cs +++ /dev/null @@ -1,79 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Features.Store; -using Microsoft.Health.Dicom.Core.Features.Workitem; -using Microsoft.Health.Dicom.Core.Messages.Workitem; -using Microsoft.Health.Dicom.Tests.Common; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Workitem; - -public sealed class CancelWorkitemResponseBuilderTests -{ - private readonly MockUrlResolver _urlResolver = new MockUrlResolver(); - private readonly DicomDataset _dataset = new DicomDataset(); - private readonly WorkitemResponseBuilder _target; - - public CancelWorkitemResponseBuilderTests() - { - _target = new WorkitemResponseBuilder(_urlResolver); - } - - [Fact] - public void GivenBuildResponse_WhenNoFailure_ThenResponseStatusIsSuccess() - { - _dataset.Add(DicomTag.SOPInstanceUID, DicomUID.Generate().UID); - - _target.AddSuccess(_dataset); - - var response = _target.BuildAddResponse(); - - Assert.NotNull(response); - Assert.Equal(WorkitemResponseStatus.Success, response.Status); - } - - [Fact] - public void GivenBuildResponse_WhenNoFailure_ThenResponseUrlIncludesWorkitemInstanceUid() - { - var workitemInstanceUid = DicomUID.Generate().UID; - _dataset.Add(DicomTag.SOPInstanceUID, workitemInstanceUid); - - _target.AddSuccess(_dataset); - - var response = _target.BuildAddResponse(); - - Assert.NotNull(response); - Assert.NotNull(response.Uri); - Assert.Contains(workitemInstanceUid, response.Uri.ToString()); - } - - [Fact] - public void GivenBuildResponse_WhenFailure_ThenFailureReasonTagIsAddedToDicomDataset() - { - _target.AddFailure((ushort)WorkitemResponseStatus.Failure, dicomDataset: _dataset); - - var response = _target.BuildAddResponse(); - - Assert.NotNull(response); - Assert.NotEmpty(_dataset.GetString(DicomTag.FailureReason)); - Assert.Null(response.Uri); - Assert.Equal(WorkitemResponseStatus.Failure, response.Status); - } - - [Fact] - public void GivenBuildResponse_WhenConflict_ThenFailureReasonTagIsAddedToDicomDataset() - { - _target.AddFailure(FailureReasonCodes.SopInstanceAlreadyExists, dicomDataset: _dataset); - - var response = _target.BuildAddResponse(); - - Assert.NotNull(response); - Assert.NotEmpty(_dataset.GetString(DicomTag.FailureReason)); - Assert.Null(response.Uri); - Assert.Equal(WorkitemResponseStatus.Conflict, response.Status); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Workitem/ChangeStateWorkitemDatasetValidatorTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Workitem/ChangeStateWorkitemDatasetValidatorTests.cs deleted file mode 100644 index b0dcdb0926..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Workitem/ChangeStateWorkitemDatasetValidatorTests.cs +++ /dev/null @@ -1,67 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Features.Store; -using Microsoft.Health.Dicom.Core.Features.Workitem; -using Microsoft.Health.Dicom.Core.Features.Workitem.Model; -using Microsoft.Health.Dicom.Tests.Common; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Workitem; - -public class ChangeStateWorkitemDatasetValidatorTests -{ - private readonly string _transactionUid; - - private readonly DicomDataset _requestDataset; - - private readonly WorkitemMetadataStoreEntry _currentWorkitem; - - public ChangeStateWorkitemDatasetValidatorTests() - { - - _transactionUid = TestUidGenerator.Generate(); - - _requestDataset = new DicomDataset - { - { DicomTag.TransactionUID, _transactionUid }, - { DicomTag.ProcedureStepState, ProcedureStepStateConstants.InProgress }, - }; - - // unclaimed workitem - _currentWorkitem = new WorkitemMetadataStoreEntry(TestUidGenerator.Generate(), 1, 1); - _currentWorkitem.ProcedureStepState = ProcedureStepState.Scheduled; - } - - [Fact] - public void GivenUnclaimedWorkitem_WhenClaiming_Succeeds() - { - var result = ChangeWorkitemStateDatasetValidator.ValidateWorkitemState(_requestDataset, _currentWorkitem); - Assert.Equal(ProcedureStepState.InProgress, result.State); - } - - [Fact] - public void GivenClaimedWorkitem_WhenCorrectTransactionUid_Succeeds() - { - _requestDataset.AddOrUpdate(DicomTag.ProcedureStepState, ProcedureStepStateConstants.Completed); - _currentWorkitem.TransactionUid = _transactionUid; - _currentWorkitem.ProcedureStepState = ProcedureStepState.InProgress; - - var result = ChangeWorkitemStateDatasetValidator.ValidateWorkitemState(_requestDataset, _currentWorkitem); - - Assert.Equal(ProcedureStepState.Completed, result.State); - } - - [Fact] - public void GivenClaimedWorkitem_WhenIncorrectTransactionUid_Throws() - { - _requestDataset.AddOrUpdate(DicomTag.ProcedureStepState, ProcedureStepStateConstants.Completed); - _currentWorkitem.TransactionUid = TestUidGenerator.Generate(); - _currentWorkitem.ProcedureStepState = ProcedureStepState.InProgress; - - Assert.Throws(() => ChangeWorkitemStateDatasetValidator.ValidateWorkitemState(_requestDataset, _currentWorkitem)); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Workitem/ProcedureStepStateExtensionsTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Workitem/ProcedureStepStateExtensionsTests.cs deleted file mode 100644 index 8c9e39585e..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Workitem/ProcedureStepStateExtensionsTests.cs +++ /dev/null @@ -1,56 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Microsoft.Health.Dicom.Core.Features.Workitem; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Workitem; - -public sealed class ProcedureStepStateExtensionsTests -{ - [Fact] - public void GivenGetTransitionState_WhenCurrentStateIsNone_ReturnsScheduledAsFutureState() - { - var result = ProcedureStepState.None.GetTransitionState(WorkitemActionEvent.NCreate); - Assert.False(result.IsError); - Assert.Equal(ProcedureStepState.Scheduled, result.State); - } - - [Theory] - [InlineData(ProcedureStepState.Scheduled)] - [InlineData(ProcedureStepState.InProgress)] - [InlineData(ProcedureStepState.Canceled)] - [InlineData(ProcedureStepState.Completed)] - public void GivenGetTransitionState_WhenCurrentStateAndFutureStateAreSame_ReturnsErrorTrue(ProcedureStepState state) - { - var result = state.GetTransitionState(WorkitemActionEvent.NCreate); - Assert.True(result.IsError); - Assert.Equal("0111", result.Code); - } - - [Theory] - [InlineData(ProcedureStepState.None, ProcedureStepStateConstants.None)] - [InlineData(ProcedureStepState.Scheduled, ProcedureStepStateConstants.Scheduled)] - [InlineData(ProcedureStepState.InProgress, ProcedureStepStateConstants.InProgress)] - [InlineData(ProcedureStepState.Completed, ProcedureStepStateConstants.Completed)] - [InlineData(ProcedureStepState.Canceled, ProcedureStepStateConstants.Canceled)] - public void GivenGetStringValue_WhenStateIsValid_ReturnsMatchingStringValue(ProcedureStepState state, string expectedValue) - { - var actual = state.GetStringValue(); - Assert.Equal(expectedValue, actual); - } - - [Theory] - [InlineData(ProcedureStepStateConstants.None, ProcedureStepState.None)] - [InlineData(ProcedureStepStateConstants.Scheduled, ProcedureStepState.Scheduled)] - [InlineData(ProcedureStepStateConstants.InProgress, ProcedureStepState.InProgress)] - [InlineData(ProcedureStepStateConstants.Completed, ProcedureStepState.Completed)] - [InlineData(ProcedureStepStateConstants.Canceled, ProcedureStepState.Canceled)] - public void GivenGetProcedureStepState_WhenValidStringValueIsPassed_ReturnsMatchingProcedureStepState(string stringValue, ProcedureStepState expectedState) - { - var actual = ProcedureStepStateExtensions.GetProcedureStepState(stringValue); - Assert.Equal(expectedState, actual); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Workitem/WorkitemFinalStateValidatorExtensionTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Workitem/WorkitemFinalStateValidatorExtensionTests.cs deleted file mode 100644 index 0acf31d56b..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Workitem/WorkitemFinalStateValidatorExtensionTests.cs +++ /dev/null @@ -1,108 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Features.Store; -using Microsoft.Health.Dicom.Core.Features.Workitem; -using Microsoft.Health.Dicom.Tests.Common; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Workitem; - -public sealed class WorkitemFinalStateValidatorExtensionTests -{ - [Fact] - public void GivenValidateFinalStateRequirement_WhenProcedureStepStateIsNotCanceledOrCompleted_ThenNoErrorsThrown() - { - var dataset = Samples.CreateRandomWorkitemInstanceDataset(); - - dataset.AddOrUpdate(DicomTag.ProcedureStepState, ProcedureStepStateConstants.Scheduled); - - WorkitemFinalStateValidatorExtension.ValidateFinalStateRequirement(dataset); - } - - [Fact] - public void GivenValidateFinalStateRequirement_WhenProcedureStepStateIsCanceledAndRequirementIsNotMet_ThenThrows() - { - var dataset = Samples.CreateRandomWorkitemInstanceDataset(); - - dataset.AddOrUpdate(DicomTag.ProcedureStepState, ProcedureStepStateConstants.Canceled); - - Assert.Throws(() => WorkitemFinalStateValidatorExtension.ValidateFinalStateRequirement(dataset)); - } - - [Fact] - public void GivenValidateFinalStateRequirement_WhenProcedureStepStateIsCanceledRequirementIsMet_ThenDoesNotThrow() - { - var dataset = Samples.CreateRandomWorkitemInstanceDataset(); - dataset.AddOrUpdate(DicomTag.SOPClassUID, TestUidGenerator.Generate()); - dataset.AddOrUpdate(DicomTag.ProcedureStepState, ProcedureStepStateConstants.Canceled); - dataset.AddOrUpdate(DicomTag.ScheduledProcedureStepModificationDateTime, DateTime.UtcNow); - - var cancelRequestDataset = Samples.CreateWorkitemCancelRequestDataset(@"Cancel from unit test"); - var fullDataset = PopulateCancelRequestAttributes(dataset, cancelRequestDataset, ProcedureStepState.Canceled); - - WorkitemFinalStateValidatorExtension.ValidateFinalStateRequirement(fullDataset); - } - - [Fact] - public void GivenValidateFinalStateRequirement_WhenProcedureStepStateIsCanceledWhenLevel3SequenceTagRequirementIsNotMet_ThenThrows() - { - var dataset = Samples.CreateRandomWorkitemInstanceDataset(); - dataset.AddOrUpdate(DicomTag.SOPClassUID, TestUidGenerator.Generate()); - dataset.AddOrUpdate(DicomTag.ProcedureStepState, ProcedureStepStateConstants.Canceled); - dataset.AddOrUpdate(DicomTag.ScheduledProcedureStepModificationDateTime, DateTime.UtcNow); - - var cancelRequestDataset = Samples.CreateWorkitemCancelRequestDataset(@"Cancel from unit test"); - var fullDataset = PopulateCancelRequestAttributes(dataset, cancelRequestDataset, ProcedureStepState.Canceled); - fullDataset.AddOrUpdate(new DicomSequence(DicomTag.ProcedureStepProgressInformationSequence, new DicomDataset - { - new DicomSequence(DicomTag.ProcedureStepDiscontinuationReasonCodeSequence, new DicomDataset()) - })); - - Assert.Throws(() => WorkitemFinalStateValidatorExtension.ValidateFinalStateRequirement(fullDataset)); - } - - private static DicomDataset PopulateCancelRequestAttributes( - DicomDataset workitemDataset, - DicomDataset cancelRequestDataset, - ProcedureStepState procedureStepState) - { - workitemDataset.AddOrUpdate(DicomTag.ProcedureStepCancellationDateTime, DateTime.UtcNow); - workitemDataset.AddOrUpdate(DicomTag.ProcedureStepState, procedureStepState.GetStringValue()); - - var cancellationReason = cancelRequestDataset.GetSingleValueOrDefault(DicomTag.ReasonForCancellation, string.Empty); - var discontinuationReasonCodeSequence = new DicomSequence(DicomTag.ProcedureStepDiscontinuationReasonCodeSequence, new DicomDataset - { - { DicomTag.ReasonForCancellation, cancellationReason } - }); - workitemDataset.AddOrUpdate(discontinuationReasonCodeSequence); - - var progressInformationSequence = new DicomSequence(DicomTag.ProcedureStepProgressInformationSequence, new DicomDataset - { - { DicomTag.ProcedureStepCancellationDateTime, DateTime.UtcNow }, - new DicomSequence(DicomTag.ProcedureStepDiscontinuationReasonCodeSequence, new DicomDataset - { - { DicomTag.ReasonForCancellation, cancellationReason } - }), - new DicomSequence(DicomTag.ProcedureStepCommunicationsURISequence, new DicomDataset - { - { DicomTag.ContactURI, cancelRequestDataset.GetSingleValueOrDefault(DicomTag.ContactURI, string.Empty) }, - { DicomTag.ContactDisplayName, cancelRequestDataset.GetSingleValueOrDefault(DicomTag.ContactDisplayName, string.Empty) }, - }) - }); - workitemDataset.AddOrUpdate(progressInformationSequence); - - // TODO: Remove this once Update workitem feature is implemented - // This is a workaround for Cancel workitem to work without Update workitem - if (cancelRequestDataset.TryGetSequence(DicomTag.UnifiedProcedureStepPerformedProcedureSequence, out var unifiedProcedureStepPerformedProcedureSequence)) - { - workitemDataset.AddOrUpdate(unifiedProcedureStepPerformedProcedureSequence); - } - - return workitemDataset; - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Workitem/WorkitemQueryParserTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Workitem/WorkitemQueryParserTests.cs deleted file mode 100644 index 0e9e9c8bcb..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Workitem/WorkitemQueryParserTests.cs +++ /dev/null @@ -1,269 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using EnsureThat; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Features.Query; -using Microsoft.Health.Dicom.Core.Features.Query.Model; -using Microsoft.Health.Dicom.Core.Features.Workitem; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Query; - -public class WorkitemQueryParserTests -{ - private readonly WorkitemQueryParser _queryParser; - - private readonly IReadOnlyList _queryTags; - - public WorkitemQueryParserTests() - { - _queryParser = new WorkitemQueryParser(new DicomTagParser()); - - _queryTags = WorkitemQueryResponseBuilder.RequiredReturnTags - .Select(x => - { - var entry = new WorkitemQueryTagStoreEntry(0, x.GetPath(), x.GetDefaultVR().Code); - entry.PathTags = Array.AsReadOnly(new DicomTag[] { x }); - return new QueryTag(entry); - }) - .ToList(); - } - - [Fact] - public void GivenParameters_WhenParsing_ThenForwardValues() - { - var parameters = new BaseQueryParameters - { - Filters = new Dictionary(), - FuzzyMatching = true, - IncludeField = Array.Empty(), - Limit = 12, - Offset = 700, - }; - - BaseQueryExpression actual = _queryParser.Parse(parameters, Array.Empty()); - Assert.Equal(parameters.FuzzyMatching, actual.FuzzyMatching); - Assert.Equal(parameters.Limit, actual.Limit); - Assert.Equal(parameters.Offset, actual.Offset); - } - - [Fact] - public void GivenIncludeField_WithValueAll_CheckAllValue() - { - BaseQueryExpression BaseQueryExpression = _queryParser.Parse( - CreateParameters(new Dictionary(), includeField: new string[] { "all" }), - _queryTags); - Assert.True(BaseQueryExpression.IncludeFields.All); - } - - [Fact] - public void GivenIncludeField_WithInvalidAttributeId_Throws() - { - Assert.Throws(() => _queryParser.Parse( - CreateParameters(new Dictionary(), includeField: new string[] { "something" }), - _queryTags)); - } - - [Theory] - [InlineData("12050010")] - [InlineData("12051001")] - public void GivenIncludeField_WithPrivateAttributeId_CheckIncludeFields(string value) - { - VerifyIncludeFieldsForValidAttributeIds(value); - } - - [Theory] - [InlineData("includefield", "12345678")] - [InlineData("includefield", "98765432")] - public void GivenIncludeField_WithUnknownAttributeId_Throws(string key, string value) - { - Assert.Throws(() => _queryParser - .Parse(CreateParameters(GetSingleton(key, value)), _queryTags)); - } - - [Theory] - [InlineData("00100010", "joe")] - [InlineData("PatientName", "joe")] - public void GivenFilterCondition_ValidTag_CheckProperties(string key, string value) - { - BaseQueryExpression BaseQueryExpression = _queryParser - .Parse(CreateParameters(GetSingleton(key, value)), _queryTags); - Assert.True(BaseQueryExpression.HasFilters); - var singleValueCond = BaseQueryExpression.FilterConditions.First() as StringSingleValueMatchCondition; - Assert.NotNull(singleValueCond); - Assert.True(singleValueCond.QueryTag.Tag == DicomTag.PatientName); - Assert.True(singleValueCond.Value == value); - } - - [Theory] - [InlineData("19510910010203", "20200220020304")] - public void GivenDateTime_WithValidRangeMatch_CheckCondition(string minValue, string maxValue) - { - EnsureArg.IsNotNull(minValue, nameof(minValue)); - EnsureArg.IsNotNull(maxValue, nameof(maxValue)); - QueryTag queryTag = new QueryTag(Tests.Common.Extensions.DicomTagExtensions.BuildWorkitemQueryTagStoreEntry("00404005", 1, "DT")); - - BaseQueryExpression BaseQueryExpression = _queryParser.Parse(CreateParameters(GetSingleton("00404005", string.Concat(minValue, "-", maxValue))), new[] { queryTag }); - var cond = BaseQueryExpression.FilterConditions.First() as DateRangeValueMatchCondition; - Assert.NotNull(cond); - Assert.True(cond.QueryTag.Tag == DicomTag.ScheduledProcedureStepStartDateTime); - Assert.True(cond.Minimum == DateTime.ParseExact(minValue, QueryParser.DateTimeTagValueFormats, null)); - Assert.True(cond.Maximum == DateTime.ParseExact(maxValue, QueryParser.DateTimeTagValueFormats, null)); - } - - [Theory] - [InlineData("", "20200220020304")] - [InlineData("19510910010203", "")] - public void GivenDateTime_WithEmptyMinOrMaxValueInRangeMatch_CheckCondition(string minValue, string maxValue) - { - EnsureArg.IsNotNull(minValue, nameof(minValue)); - EnsureArg.IsNotNull(maxValue, nameof(maxValue)); - QueryTag queryTag = new QueryTag(Tests.Common.Extensions.DicomTagExtensions.BuildWorkitemQueryTagStoreEntry("00404005", 1, "DT")); - - BaseQueryExpression BaseQueryExpression = _queryParser.Parse(CreateParameters(GetSingleton("00404005", string.Concat(minValue, "-", maxValue))), new[] { queryTag }); - var cond = BaseQueryExpression.FilterConditions.First() as DateRangeValueMatchCondition; - Assert.NotNull(cond); - Assert.Equal(DicomTag.ScheduledProcedureStepStartDateTime, cond.QueryTag.Tag); - - DateTime expectedMin = string.IsNullOrEmpty(minValue) ? DateTime.MinValue : DateTime.ParseExact(minValue, QueryParser.DateTimeTagValueFormats, null); - DateTime expectedMax = string.IsNullOrEmpty(maxValue) ? DateTime.MaxValue : DateTime.ParseExact(maxValue, QueryParser.DateTimeTagValueFormats, null); - Assert.Equal(expectedMin, cond.Minimum); - Assert.Equal(expectedMax, cond.Maximum); - } - - [Fact] - public void GivenDateTime_WithEmptyMinAndMaxInRangeMatch_Throw() - { - QueryTag queryTag = new QueryTag(Tests.Common.Extensions.DicomTagExtensions.BuildWorkitemQueryTagStoreEntry("00404005", 1, "DT")); - Assert.Throws(() => _queryParser - .Parse(CreateParameters(GetSingleton("DateTime", "-")), new[] { queryTag })); - } - - [Fact] - public void GivenFilterCondition_WithDuplicateQueryParam_Throws() - { - Assert.Throws(() => _queryParser.Parse( - CreateParameters( - new Dictionary - { - { "PatientName", "Joe" }, - { "00100010", "Rob" }, - }), - _queryTags)); - } - - [Theory] - [InlineData("PatientName", " ")] - [InlineData("PatientName", "")] - public void GivenFilterCondition_WithInvalidAttributeIdStringValue_Throws(string key, string value) - { - Assert.Throws(() => _queryParser - .Parse(CreateParameters(GetSingleton(key, value)), _queryTags)); - } - - [Theory] - [InlineData("00390061", "invalidtag")] - [InlineData("unkownparam", "invalidtag")] - public void GivenFilterCondition_WithInvalidAttributeId_Throws(string key, string value) - { - Assert.Throws(() => _queryParser - .Parse(CreateParameters(GetSingleton(key, value)), _queryTags)); - } - - [Theory] - [InlineData("0040A370.00080050", "Foo")] - public void GivenWorkitemQueryTag_WithValidValue_ThenReturnsSuccessfully(string key, string value) - { - EnsureArg.IsNotNull(key, nameof(key)); - EnsureArg.IsNotNull(value, nameof(value)); - var item = new DicomSequence( - DicomTag.ReferencedRequestSequence, - new DicomDataset[] { - new DicomDataset( - new DicomShortString( - DicomTag.AccessionNumber, "Foo"), - new DicomShortString( - DicomTag.RequestedProcedureID, "Bar"))}); - QueryTag[] tags = new QueryTag[] - { - new QueryTag(Tests.Common.Extensions.DicomTagExtensions.BuildWorkitemQueryTagStoreEntry("0040A370.00080050", 1, item.ValueRepresentation.Code)) - }; - - var expectedQueryTag = new QueryTag(Tests.Common.Extensions.DicomTagExtensions.BuildWorkitemQueryTagStoreEntry("00080050", 1, DicomTag.AccessionNumber.GetDefaultVR().Code)); - BaseQueryExpression BaseQueryExpression = _queryParser - .Parse(CreateParameters(GetSingleton(key, value)), tags); - Assert.Equal(expectedQueryTag.Tag, BaseQueryExpression.FilterConditions.First().QueryTag.Tag); - } - - [Fact] - public void GivenTwoWorkitemQueryTags_WithSameLastLevelKey_ThenReturnsSuccessfully() - { - var item1 = new DicomSequence( - DicomTag.ScheduledStationClassCodeSequence, - new DicomDataset[] { - new DicomDataset( - new DicomShortString( - DicomTag.CodeValue, "Foo"))}); - - var item2 = new DicomSequence( - DicomTag.ScheduledStationNameCodeSequence, - new DicomDataset[] { - new DicomDataset( - new DicomShortString( - DicomTag.CodeValue, "Bar"))}); - - QueryTag[] tags = new QueryTag[] - { - new QueryTag(Tests.Common.Extensions.DicomTagExtensions.BuildWorkitemQueryTagStoreEntry("00404025.00080100", 1, item1.ValueRepresentation.Code)), - new QueryTag(Tests.Common.Extensions.DicomTagExtensions.BuildWorkitemQueryTagStoreEntry("00404026.00080100", 2, item2.ValueRepresentation.Code)) - }; - - var expectedQueryTag = new QueryTag(Tests.Common.Extensions.DicomTagExtensions.BuildWorkitemQueryTagStoreEntry("00080100", 1, DicomTag.AccessionNumber.GetDefaultVR().Code)); - var filterConditions = new Dictionary(); - filterConditions.Add("00404025.00080100", "Foo"); - filterConditions.Add("00404026.00080100", "Bar"); - - BaseQueryExpression BaseQueryExpression = _queryParser - .Parse(CreateParameters(filterConditions), tags); - - Assert.Equal(2, BaseQueryExpression.FilterConditions.Count); - Assert.Equal(expectedQueryTag.Tag, BaseQueryExpression.FilterConditions.First().QueryTag.Tag); - Assert.Equal(expectedQueryTag.Tag, BaseQueryExpression.FilterConditions.Last().QueryTag.Tag); - } - - private void VerifyIncludeFieldsForValidAttributeIds(params string[] values) - { - BaseQueryExpression BaseQueryExpression = _queryParser.Parse( - CreateParameters(new Dictionary(), includeField: values), - _queryTags); - - Assert.False(BaseQueryExpression.HasFilters); - Assert.False(BaseQueryExpression.IncludeFields.All); - Assert.Equal(values.Length, BaseQueryExpression.IncludeFields.DicomTags.Count); - } - - private static Dictionary GetSingleton(string key, string value) - => new Dictionary { { key, value } }; - - private static BaseQueryParameters CreateParameters( - Dictionary filters, - bool fuzzyMatching = false, - string[] includeField = null) - { - return new BaseQueryParameters - { - Filters = filters, - FuzzyMatching = fuzzyMatching, - IncludeField = includeField ?? Array.Empty(), - }; - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Workitem/WorkitemQueryResponseBuilderTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Workitem/WorkitemQueryResponseBuilderTests.cs deleted file mode 100644 index 181b322f31..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Workitem/WorkitemQueryResponseBuilderTests.cs +++ /dev/null @@ -1,106 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Linq; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Features.Query; -using Microsoft.Health.Dicom.Core.Features.Query.Model; -using Microsoft.Health.Dicom.Core.Features.Workitem; -using Microsoft.Health.Dicom.Core.Messages.Workitem; -using Microsoft.Health.Dicom.Tests.Common; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Query; - -public class WorkitemQueryResponseBuilderTests -{ - [Fact] - public void GivenWorkitem_WithIncludeField_ValidReturned() - { - var includeField = new QueryIncludeField(new List { DicomTag.WorklistLabel }); - var filters = new List() - { - new StringSingleValueMatchCondition(new QueryTag(DicomTag.PatientName), "Foo"), - }; - var query = new BaseQueryExpression(includeField, false, 0, 0, filters); - var dataset = Samples - .CreateRandomWorkitemInstanceDataset() - .AddOrUpdate(new DicomLongString(DicomTag.MedicalAlerts)) - .AddOrUpdate(new DicomShortString(DicomTag.SnoutID)); - - var datasets = new List - { - dataset - }; - - var response = WorkitemQueryResponseBuilder.BuildWorkitemQueryResponse(datasets, query); - DicomDataset responseDataset = response.ResponseDatasets.FirstOrDefault(); - - Assert.NotNull(responseDataset); - var tags = responseDataset.Select(i => i.Tag).ToList(); - - Assert.Contains(DicomTag.WorklistLabel, tags); // Valid include - Assert.Contains(DicomTag.PatientName, tags); // Valid filter - Assert.Contains(DicomTag.MedicalAlerts, tags); // Required return attribute - - Assert.DoesNotContain(DicomTag.TransactionUID, tags); // should never be included - Assert.DoesNotContain(DicomTag.SnoutID, tags); // Not a required return attribute - } - - [Fact] - public void GivenWorkitem_WithIncludeFieldAll_AllReturned() - { - var includeField = QueryIncludeField.AllFields; - var filters = new List(); - var query = new BaseQueryExpression(includeField, false, 0, 0, filters); - var dataset = Samples - .CreateRandomWorkitemInstanceDataset() - .AddOrUpdate(new DicomLongString(DicomTag.MedicalAlerts)) - .AddOrUpdate(new DicomShortString(DicomTag.SnoutID)); - - var datasets = new List - { - dataset - }; - - var response = WorkitemQueryResponseBuilder.BuildWorkitemQueryResponse(datasets, query); - DicomDataset responseDataset = response.ResponseDatasets.FirstOrDefault(); - - Assert.NotNull(responseDataset); - var tags = responseDataset.Select(i => i.Tag).ToList(); - - Assert.Contains(DicomTag.MedicalAlerts, tags); // Required return attribute - Assert.Contains(DicomTag.SnoutID, tags); // Not a required return attribute - set by 'all' - - Assert.DoesNotContain(DicomTag.TransactionUID, tags); // should never be included - } - - [Fact] - public void GivenWorkitem_WithIncludeFieldAndPartialMatchingResult_ValidPartialContentReturned() - { - var includeField = new QueryIncludeField(new List { DicomTag.WorklistLabel }); - var filters = new List() - { - new StringSingleValueMatchCondition(new QueryTag(DicomTag.PatientName), "Foo"), - }; - var query = new BaseQueryExpression(includeField, false, 0, 0, filters); - var dataset = Samples - .CreateRandomWorkitemInstanceDataset() - .AddOrUpdate(new DicomLongString(DicomTag.MedicalAlerts)) - .AddOrUpdate(new DicomShortString(DicomTag.SnoutID)); - - var datasets = new List - { - dataset, - null - }; - - var response = WorkitemQueryResponseBuilder.BuildWorkitemQueryResponse(datasets, query); - Assert.Single(response.ResponseDatasets); - Assert.Equal(WorkitemResponseStatus.PartialContent, response.Status); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Workitem/WorkitemQueryTagServiceTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Workitem/WorkitemQueryTagServiceTests.cs deleted file mode 100644 index 04490fd9a5..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Workitem/WorkitemQueryTagServiceTests.cs +++ /dev/null @@ -1,40 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Workitem; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.ExtendedQueryTag; - -public class WorkitemQueryTagServiceTests -{ - private readonly IIndexWorkitemStore _indexWorkitemStore; - private readonly IWorkitemQueryTagService _queryTagService; - private readonly IDicomTagParser _dicomTagParser; - - public WorkitemQueryTagServiceTests() - { - _indexWorkitemStore = Substitute.For(); - _dicomTagParser = Substitute.For(); - _queryTagService = new WorkitemQueryTagService(_indexWorkitemStore, _dicomTagParser, NullLogger.Instance); - } - - [Fact] - public async Task GivenValidInput_WhenGetWorkitemQueryTagsIsCalledMultipleTimes_ThenWorkitemStoreIsCalledOnce() - { - _indexWorkitemStore.GetWorkitemQueryTagsAsync(Arg.Any()) - .Returns(Array.Empty()); - - await _queryTagService.GetQueryTagsAsync(); - await _queryTagService.GetQueryTagsAsync(); - await _indexWorkitemStore.Received(1).GetWorkitemQueryTagsAsync(Arg.Any()); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Workitem/WorkitemRequestValidatorExtensionsTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Workitem/WorkitemRequestValidatorExtensionsTests.cs deleted file mode 100644 index ae77a4b50d..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Workitem/WorkitemRequestValidatorExtensionsTests.cs +++ /dev/null @@ -1,56 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Workitem; -using Microsoft.Health.Dicom.Core.Messages.Workitem; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Workitem; - -public sealed class WorkitemRequestValidatorExtensionsTests -{ - [Fact] - public void GivenAddWorkitemRequest_WhenRequestBodyIsNull_ThenBadRequestExceptionIsThrown() - { - var target = new AddWorkitemRequest(null, @"application/json", Guid.NewGuid().ToString()); - - Assert.Throws(() => target.Validate()); - } - - [Fact] - public void GivenAddWorkitemRequest_WhenWorkitemInstanceUidIsNull_ThenNoExceptionIsThrown() - { - var target = new AddWorkitemRequest(new DicomDataset(), @"application/json", null); - - target.Validate(); - } - - [Fact] - public void GivenAddWorkitemRequest_WhenWorkitemInstanceUidIsLongerThan64Chars_ThenInvalidIdentifierExceptionIsThrown() - { - var target = new AddWorkitemRequest(new DicomDataset(), @"application/json", Guid.NewGuid().ToString()); - - Assert.Throws(() => target.Validate()); - } - - [Fact] - public void GivenAddWorkitemRequest_WhenWorkitemInstanceUidIsInvalid_ThenInvalidIdentifierExceptionIsThrown() - { - var target = new AddWorkitemRequest(new DicomDataset(), @"application/json", @"12346.8234234.abc.234"); - - Assert.Throws(() => target.Validate()); - } - - [Fact] - public void GivenAddWorkitemRequest_WhenWorkitemInstanceUidIsValid_ThenNoExceptionIsThrown() - { - var target = new AddWorkitemRequest(new DicomDataset(), @"application/json", @"12346.8234234.234"); - - target.Validate(); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Workitem/WorkitemServiceTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Workitem/WorkitemServiceTests.cs deleted file mode 100644 index 6f9e5e3881..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Workitem/WorkitemServiceTests.cs +++ /dev/null @@ -1,643 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using FellowOakDicom; -using Microsoft.Extensions.Logging; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Core.Features.Query; -using Microsoft.Health.Dicom.Core.Features.Store; -using Microsoft.Health.Dicom.Core.Features.Validation; -using Microsoft.Health.Dicom.Core.Features.Workitem; -using Microsoft.Health.Dicom.Core.Features.Workitem.Model; -using Microsoft.Health.Dicom.Tests.Common; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Features.Workitem; - -public sealed class WorkitemServiceTests -{ - private readonly IWorkitemDatasetValidator _addDatasetValidator = Substitute.For(); - private readonly IWorkitemDatasetValidator _cancelDatasetValidator = Substitute.For(); - private readonly IWorkitemResponseBuilder _responseBuilder = Substitute.For(); - private readonly IWorkitemOrchestrator _orchestrator = Substitute.For(); - private readonly ILogger _logger = Substitute.For>(); - private readonly DicomDataset _dataset = new DicomDataset(); - private readonly WorkitemService _target; - - public WorkitemServiceTests() - { - _addDatasetValidator.Name.Returns(typeof(AddWorkitemDatasetValidator).Name); - _cancelDatasetValidator.Name.Returns(typeof(CancelWorkitemDatasetValidator).Name); - - _target = new WorkitemService(_responseBuilder, new[] - { - _addDatasetValidator, - _cancelDatasetValidator - }, _orchestrator, _logger); - - _dataset.Add(DicomTag.ProcedureStepState, string.Empty); - } - - [Fact] - public async Task GivenNullDicomDataset_WhenProcessed_ThenArgumentNullExceptionIsThrown() - { - await Assert.ThrowsAsync(() => _target.ProcessAddAsync(null, string.Empty, CancellationToken.None)); - } - - [Fact] - public async Task GivenValidWorkitemInstanceUid_WhenProcessed_ThenIsSetupForSOPInstanceUIDTagInTheDataset() - { - var workitemInstanceUid = DicomUID.Generate().UID; - - _dataset.Add(DicomTag.SOPInstanceUID, workitemInstanceUid); - - await _target.ProcessAddAsync(_dataset, workitemInstanceUid, CancellationToken.None); - - Assert.Equal(workitemInstanceUid, _dataset.GetString(DicomTag.SOPInstanceUID)); - } - - [Fact] - public async Task GivenValidWorkitemInstanceUidInDicomTagSOPInstanceUID_WhenProcessed_ThenIsSetupForSOPInstanceUIDTagInTheDataset() - { - var workitemInstanceUid = DicomUID.Generate().UID; - - _dataset.Add(DicomTag.SOPInstanceUID, workitemInstanceUid); - - await _target.ProcessAddAsync(_dataset, string.Empty, CancellationToken.None); - - Assert.Equal(workitemInstanceUid, _dataset.GetString(DicomTag.SOPInstanceUID)); - } - - [Fact] - public async Task GivenValidDicomDataset_WhenProcessed_ThenCallsValidate() - { - var workitemInstanceUid = DicomUID.Generate().UID; - - _dataset.Add(DicomTag.SOPInstanceUID, workitemInstanceUid); - - await _target.ProcessAddAsync(_dataset, workitemInstanceUid, CancellationToken.None); - - _addDatasetValidator - .Received() - .Validate(Arg.Is(ds => ReferenceEquals(ds, _dataset))); - } - - [Fact] - public async Task GivenValidateThrowsDatasetValidationException_WhenProcessed_ThenWorkitemOrchestratorAddWorkitemIsNotCalled() - { - var workitemInstanceUid = DicomUID.Generate().UID; - - _dataset.Add(DicomTag.SOPInstanceUID, workitemInstanceUid); - - _addDatasetValidator - .When(dv => dv.Validate(Arg.Any())) - .Throw(new DatasetValidationException(ushort.MinValue, string.Empty)); - - await _target.ProcessAddAsync(_dataset, string.Empty, CancellationToken.None); - - await _orchestrator - .DidNotReceive() - .AddWorkitemAsync(Arg.Any(), Arg.Any()); - } - - [Fact] - public async Task GivenValidateThrowsDatasetValidationException_WhenProcessed_ThenResponseBuilderAddFailureIsCalled() - { - var failureCode = FailureReasonCodes.ValidationFailure; - var workitemInstanceUid = DicomUID.Generate().UID; - var errorMessage = @"Unit Test - Failed validation"; - - _dataset.Add(DicomTag.SOPInstanceUID, workitemInstanceUid); - - _addDatasetValidator - .When(dv => dv.Validate(Arg.Any())) - .Throw(new DatasetValidationException(failureCode, errorMessage)); - - await _target.ProcessAddAsync(_dataset, string.Empty, CancellationToken.None); - - _responseBuilder - .Received() - .AddFailure( - Arg.Is(fc => fc == failureCode), - Arg.Is(msg => msg == errorMessage), - Arg.Is(ds => ReferenceEquals(ds, _dataset))); - } - - [Fact] - public async Task GivenValidateThrowsException_WhenProcessed_ThenResponseBuilderAddFailureIsCalledWithProcessingFailureError() - { - var workitemInstanceUid = DicomUID.Generate().UID; - var errorMessage = @"Unit Test - Failed validation"; - - _dataset.Add(DicomTag.SOPInstanceUID, workitemInstanceUid); - - _addDatasetValidator - .When(dv => dv.Validate(Arg.Any())) - .Throw(new Exception(errorMessage)); - - await _target.ProcessAddAsync(_dataset, string.Empty, CancellationToken.None); - - _responseBuilder - .Received() - .AddFailure( - Arg.Is(fc => fc == FailureReasonCodes.ProcessingFailure), - Arg.Is(msg => msg == errorMessage), - Arg.Is(ds => ReferenceEquals(ds, _dataset))); - } - - [Fact] - public async Task GivenWorkitemOrchestratorThrowsWorkitemAlreadyExistsException_WhenProcessed_ThenResponseBuilderAddFailureIsCalled() - { - var failureCode = FailureReasonCodes.SopInstanceAlreadyExists; - - var workitemInstanceUid = DicomUID.Generate().UID; - - _dataset.Add(DicomTag.SOPInstanceUID, workitemInstanceUid); - - _orchestrator - .When(orc => orc.AddWorkitemAsync(Arg.Is(ds => ReferenceEquals(ds, _dataset)), Arg.Any())) - .Throw(new WorkitemAlreadyExistsException()); - - await _target.ProcessAddAsync(_dataset, string.Empty, CancellationToken.None); - - _responseBuilder - .Received() - .AddFailure( - Arg.Is(fc => fc == failureCode), - Arg.Is(msg => msg == DicomCoreResource.WorkitemInstanceAlreadyExists), - Arg.Is(ds => ReferenceEquals(ds, _dataset))); - } - - [Fact] - public async Task GivenWorkitemOrchestratorThrowsException_WhenProcessed_ThenResponseBuilderAddFailureIsCalled() - { - var failureCode = FailureReasonCodes.ProcessingFailure; - - var workitemInstanceUid = DicomUID.Generate().UID; - - _dataset.Add(DicomTag.SOPInstanceUID, workitemInstanceUid); - - _orchestrator - .When(orc => orc.AddWorkitemAsync(Arg.Is(ds => ReferenceEquals(ds, _dataset)), Arg.Any())) - .Throw(new Exception(workitemInstanceUid)); - - await _target.ProcessAddAsync(_dataset, string.Empty, CancellationToken.None); - - _responseBuilder - .Received() - .AddFailure( - Arg.Is(fc => fc == failureCode), - Arg.Is(msg => msg == workitemInstanceUid), - Arg.Is(ds => ReferenceEquals(ds, _dataset))); - } - - [Fact] - public async Task GivenDicomDataset_WhenProcessed_ThenResponseBuilderBuildResponseIsAlwaysCalled() - { - _addDatasetValidator - .When(dv => dv.Validate(Arg.Any())) - .Throw(new ElementValidationException(string.Empty, DicomVR.UN, ValidationErrorCode.UnexpectedVR)); - - await _target.ProcessAddAsync(new DicomDataset(), string.Empty, CancellationToken.None); - - _responseBuilder.Received().BuildAddResponse(); - - _addDatasetValidator - .When(dv => dv.Validate(Arg.Any())) - .Throw(new ElementValidationException(string.Empty, DicomVR.UN, ValidationErrorCode.UnexpectedVR)); - - await _target.ProcessAddAsync(_dataset, string.Empty, CancellationToken.None); - - _responseBuilder.Received().BuildAddResponse(); - } - - [Fact] - public async Task GivenWorkitemStoreSucceeded_WhenProcessed_ThenResponseBuilderAddSuccessIsCalled() - { - await _target.ProcessAddAsync(_dataset, string.Empty, CancellationToken.None); - - _responseBuilder.Received().AddSuccess(Arg.Is(ds => ReferenceEquals(ds, _dataset))); - } - - [Fact] - public async Task GivenProcessCancel_WhenWorkitemIsNotFound_ThenResponseBuilderAddFailureIsCalled() - { - var workitemInstanceUid = DicomUID.Generate().UID; - _dataset.AddOrUpdate(DicomTag.SOPInstanceUID, workitemInstanceUid); - - _orchestrator - .GetWorkitemMetadataAsync(Arg.Is(uid => string.Equals(workitemInstanceUid, uid)), Arg.Any()) - .Returns(Task.FromResult(null as WorkitemMetadataStoreEntry)); - - await _target.ProcessCancelAsync(_dataset, workitemInstanceUid, CancellationToken.None); - - _responseBuilder.Received() - .AddFailure( - Arg.Is(fc => fc == FailureReasonCodes.UpsInstanceNotFound), - Arg.Is(v => string.Equals(v, DicomCoreResource.WorkitemInstanceNotFound)), - Arg.Is(ds => ReferenceEquals(ds, _dataset))); - } - - [Fact] - public async Task GivenProcessCancel_WhenWorkitemIsNotFound_ThenResponseBuilderBuildResponseIsCalled() - { - var workitemInstanceUid = DicomUID.Generate().UID; - _dataset.AddOrUpdate(DicomTag.SOPInstanceUID, workitemInstanceUid); - - _orchestrator - .GetWorkitemMetadataAsync(Arg.Is(uid => string.Equals(workitemInstanceUid, uid)), Arg.Any()) - .Returns(Task.FromResult(null as WorkitemMetadataStoreEntry)); - - await _target.ProcessCancelAsync(_dataset, workitemInstanceUid, CancellationToken.None); - - _responseBuilder.Received().BuildCancelResponse(); - } - - [Fact] - public async Task GivenProcessCancel_WhenGetWorkitemBlobAsyncFails_Throws() - { - var workitemInstanceUid = DicomUID.Generate().UID; - _dataset.AddOrUpdate(DicomTag.SOPInstanceUID, workitemInstanceUid); - - _orchestrator - .GetWorkitemMetadataAsync(Arg.Is(uid => string.Equals(workitemInstanceUid, uid)), Arg.Any()) - .Returns(Task.FromResult(new WorkitemMetadataStoreEntry(workitemInstanceUid, 1, 101, Partition.DefaultKey))); - - _orchestrator - .When(orc => orc.GetWorkitemBlobAsync(Arg.Any(), Arg.Any())) - .Throw(new ArgumentException(@"Error thrown from the test mock")); - - await Assert.ThrowsAsync(() => _target.ProcessCancelAsync(_dataset, workitemInstanceUid, CancellationToken.None)); - } - - [Fact] - public async Task GivenProcessCancel_WhenValidationSucceeds_ThenResponseBuilderAddSuccessIsCalled() - { - var workitemInstanceUid = DicomUID.Generate().UID; - _dataset.AddOrUpdate(DicomTag.SOPInstanceUID, workitemInstanceUid); - _dataset.AddOrUpdate(DicomTag.ProcedureStepState, ProcedureStepStateConstants.Scheduled); - - _orchestrator - .GetWorkitemMetadataAsync(Arg.Is(uid => string.Equals(workitemInstanceUid, uid)), Arg.Any()) - .Returns(Task.FromResult(new WorkitemMetadataStoreEntry(workitemInstanceUid, 1, 101, Partition.DefaultKey) - { - ProcedureStepState = ProcedureStepState.Scheduled, - Status = WorkitemStoreStatus.ReadWrite - })); - - _orchestrator - .GetWorkitemBlobAsync(Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(_dataset)); - - var cancelRequestDataset = Samples.CreateWorkitemCancelRequestDataset(@"Cancel from Unit Test"); - await _target.ProcessCancelAsync(cancelRequestDataset, workitemInstanceUid, CancellationToken.None); - - _responseBuilder.Received().AddSuccess(Arg.Any()); - } - - [Fact] - public async Task GivenProcessCancel_AlwaysCallsCancelWorkitemDatasetValidator() - { - var workitemInstanceUid = DicomUID.Generate().UID; - _dataset.AddOrUpdate(DicomTag.SOPInstanceUID, workitemInstanceUid); - _dataset.AddOrUpdate(DicomTag.ProcedureStepState, ProcedureStepStateConstants.Scheduled); - - _orchestrator - .GetWorkitemMetadataAsync(Arg.Is(uid => string.Equals(workitemInstanceUid, uid)), Arg.Any()) - .Returns(Task.FromResult(new WorkitemMetadataStoreEntry(workitemInstanceUid, 1, 101, Partition.DefaultKey) - { - ProcedureStepState = ProcedureStepState.Scheduled, - Status = WorkitemStoreStatus.ReadWrite - })); - - _orchestrator - .GetWorkitemBlobAsync(Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(_dataset)); - - var cancelRequestDataset = Samples.CreateWorkitemCancelRequestDataset(@"Cancel from Unit Test"); - await _target.ProcessCancelAsync(cancelRequestDataset, workitemInstanceUid, CancellationToken.None); - - _cancelDatasetValidator.Received().Validate(Arg.Any()); - } - - [Fact] - public async Task GivenProcessCancel_WhenCancelWorkitemDatasetValidatorFailsWithDatasetValidationException_ThenResponseBuilderAddFailureIsCalled() - { - var failureCode = FailureReasonCodes.UpsIsAlreadyCanceled; - var workitemInstanceUid = DicomUID.Generate().UID; - _dataset.AddOrUpdate(DicomTag.SOPInstanceUID, workitemInstanceUid); - _dataset.AddOrUpdate(DicomTag.ProcedureStepState, ProcedureStepStateConstants.Scheduled); - - _orchestrator - .GetWorkitemMetadataAsync(Arg.Is(uid => string.Equals(workitemInstanceUid, uid)), Arg.Any()) - .Returns(Task.FromResult(new WorkitemMetadataStoreEntry(workitemInstanceUid, 1, 101, Partition.DefaultKey) - { - ProcedureStepState = ProcedureStepState.Scheduled, - Status = WorkitemStoreStatus.ReadWrite - })); - - _orchestrator - .GetWorkitemBlobAsync(Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(_dataset)); - - _cancelDatasetValidator - .When(v => v.Validate(Arg.Any())) - .Throw(new DatasetValidationException(failureCode, @"Failure from Unit Test")); - - var cancelRequestDataset = Samples.CreateWorkitemCancelRequestDataset(@"Cancel from Unit Test"); - await _target.ProcessCancelAsync(cancelRequestDataset, workitemInstanceUid, CancellationToken.None); - - _responseBuilder - .Received() - .AddFailure( - Arg.Is(fc => fc == failureCode), - Arg.Is(msg => msg == @"Failure from Unit Test"), - Arg.Any()); - } - - [Fact] - public async Task GivenProcessCancel_WhenCancelWorkitemDatasetValidatorFailsWithDicomValidationException_ThenResponseBuilderAddFailureIsCalled() - { - var workitemInstanceUid = DicomUID.Generate().UID; - _dataset.AddOrUpdate(DicomTag.SOPInstanceUID, workitemInstanceUid); - _dataset.AddOrUpdate(DicomTag.ProcedureStepState, ProcedureStepStateConstants.Scheduled); - - _orchestrator - .GetWorkitemMetadataAsync(Arg.Is(uid => string.Equals(workitemInstanceUid, uid)), Arg.Any()) - .Returns(Task.FromResult(new WorkitemMetadataStoreEntry(workitemInstanceUid, 1, 101, Partition.DefaultKey) - { - ProcedureStepState = ProcedureStepState.Scheduled, - Status = WorkitemStoreStatus.ReadWrite - })); - - _orchestrator - .GetWorkitemBlobAsync(Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(_dataset)); - - _cancelDatasetValidator - .When(v => v.Validate(Arg.Any())) - .Throw(new DicomValidationException(@"Error content", DicomVR.ST, @"Failure from Unit Test")); - - var cancelRequestDataset = Samples.CreateWorkitemCancelRequestDataset(@"Cancel from Unit Test"); - await _target.ProcessCancelAsync(cancelRequestDataset, workitemInstanceUid, CancellationToken.None); - - _responseBuilder - .Received() - .AddFailure( - Arg.Is(fc => fc == FailureReasonCodes.UpsInstanceUpdateNotAllowed), - Arg.Is(msg => msg == "Content \"Error content\" does not validate VR ST: Failure from Unit Test"), - Arg.Any()); - } - - [Fact] - public async Task GivenProcessCancel_WhenCancelWorkitemDatasetValidatorFailsWithValidationException_ThenResponseBuilderAddFailureIsCalled() - { - var workitemInstanceUid = DicomUID.Generate().UID; - _dataset.AddOrUpdate(DicomTag.SOPInstanceUID, workitemInstanceUid); - _dataset.AddOrUpdate(DicomTag.ProcedureStepState, ProcedureStepStateConstants.Scheduled); - - _orchestrator - .GetWorkitemMetadataAsync(Arg.Is(uid => string.Equals(workitemInstanceUid, uid)), Arg.Any()) - .Returns(Task.FromResult(new WorkitemMetadataStoreEntry(workitemInstanceUid, 1, 101, Partition.DefaultKey) - { - ProcedureStepState = ProcedureStepState.Scheduled, - Status = WorkitemStoreStatus.ReadWrite - })); - - _orchestrator - .GetWorkitemBlobAsync(Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(_dataset)); - - _cancelDatasetValidator - .When(v => v.Validate(Arg.Any())) - .Throw(Substitute.For(@"Failure from Unit Test")); - - var cancelRequestDataset = Samples.CreateWorkitemCancelRequestDataset(@"Cancel from Unit Test"); - await _target.ProcessCancelAsync(cancelRequestDataset, workitemInstanceUid, CancellationToken.None); - - _responseBuilder - .Received() - .AddFailure( - Arg.Is(fc => fc == FailureReasonCodes.UpsInstanceUpdateNotAllowed), - Arg.Any(), - Arg.Any()); - } - - [Fact] - public async Task GivenProcessCancel_WhenCancelWorkitemDatasetValidatorFailsWithWorkitemNotFoundException_ThenResponseBuilderAddFailureIsCalled() - { - var workitemInstanceUid = DicomUID.Generate().UID; - _dataset.AddOrUpdate(DicomTag.SOPInstanceUID, workitemInstanceUid); - _dataset.AddOrUpdate(DicomTag.ProcedureStepState, ProcedureStepStateConstants.Scheduled); - - _orchestrator - .GetWorkitemMetadataAsync(Arg.Is(uid => string.Equals(workitemInstanceUid, uid)), Arg.Any()) - .Returns(Task.FromResult(new WorkitemMetadataStoreEntry(workitemInstanceUid, 1, 101, Partition.DefaultKey) - { - ProcedureStepState = ProcedureStepState.Scheduled, - Status = WorkitemStoreStatus.ReadWrite - })); - - _orchestrator - .GetWorkitemBlobAsync(Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(_dataset)); - - var exception = new WorkitemNotFoundException(); - _cancelDatasetValidator - .When(v => v.Validate(Arg.Any())) - .Throw(exception); - - var cancelRequestDataset = Samples.CreateWorkitemCancelRequestDataset(@"Cancel from Unit Test"); - await _target.ProcessCancelAsync(cancelRequestDataset, workitemInstanceUid, CancellationToken.None); - - _responseBuilder - .Received() - .AddFailure( - Arg.Is(fc => fc == FailureReasonCodes.UpsInstanceNotFound), - Arg.Is(msg => msg == exception.Message), - Arg.Any()); - } - - [Fact] - public async Task GivenProcessCancel_WhenCancelWorkitemDatasetValidatorFailsWithUnknownException_ThenResponseBuilderAddFailureIsCalled() - { - var workitemInstanceUid = DicomUID.Generate().UID; - _dataset.AddOrUpdate(DicomTag.SOPInstanceUID, workitemInstanceUid); - _dataset.AddOrUpdate(DicomTag.ProcedureStepState, ProcedureStepStateConstants.Scheduled); - - _orchestrator - .GetWorkitemMetadataAsync(Arg.Is(uid => string.Equals(workitemInstanceUid, uid)), Arg.Any()) - .Returns(Task.FromResult(new WorkitemMetadataStoreEntry(workitemInstanceUid, 1, 101, Partition.DefaultKey) - { - ProcedureStepState = ProcedureStepState.Scheduled, - Status = WorkitemStoreStatus.ReadWrite - })); - - _orchestrator - .GetWorkitemBlobAsync(Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(_dataset)); - - var exception = new Exception(@"Failure from unit test"); - _cancelDatasetValidator - .When(v => v.Validate(Arg.Any())) - .Throw(exception); - - var cancelRequestDataset = Samples.CreateWorkitemCancelRequestDataset(@"Cancel from Unit Test"); - await _target.ProcessCancelAsync(cancelRequestDataset, workitemInstanceUid, CancellationToken.None); - - _responseBuilder - .Received() - .AddFailure( - Arg.Is(fc => fc == FailureReasonCodes.ProcessingFailure), - Arg.Is(msg => msg == exception.Message), - Arg.Any()); - } - - [Fact] - public async Task GivenProcessCancel_WhenUpdateWorkitemFails_ThenResponseBuilderAddFailureIsCalled() - { - var workitemInstanceUid = DicomUID.Generate().UID; - _dataset.AddOrUpdate(DicomTag.SOPInstanceUID, workitemInstanceUid); - _dataset.AddOrUpdate(DicomTag.ProcedureStepState, ProcedureStepStateConstants.Scheduled); - - _orchestrator - .GetWorkitemMetadataAsync(Arg.Is(uid => string.Equals(workitemInstanceUid, uid)), Arg.Any()) - .Returns(Task.FromResult(new WorkitemMetadataStoreEntry(workitemInstanceUid, 1, 101, Partition.DefaultKey) - { - ProcedureStepState = ProcedureStepState.Scheduled, - Status = WorkitemStoreStatus.ReadWrite - })); - - _orchestrator - .GetWorkitemBlobAsync(Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(_dataset)); - - var cancelRequestDataset = Samples.CreateWorkitemCancelRequestDataset(@"Cancel from Unit Test"); - - _orchestrator - .When(orc => orc.UpdateWorkitemStateAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any())) - .Throw(new Exception(@"Failure from unit tests")); - - await _target.ProcessCancelAsync(cancelRequestDataset, workitemInstanceUid, CancellationToken.None); - - _responseBuilder - .Received() - .AddFailure( - Arg.Is(fc => fc == FailureReasonCodes.ProcessingFailure), - Arg.Any(), - Arg.Any()); - } - - [Theory] - [MemberData(nameof(AddedAttributesForCreate))] - internal void GivenEmptyDataset_AttributesAreAdded(DicomTag tag) - { - var dataset = new DicomDataset(); - - WorkitemService.SetSpecifiedAttributesForCreate(dataset, string.Empty); - - Assert.True(dataset.Contains(tag)); - Assert.True(dataset.GetValueCount(tag) > 0); - } - - public static IEnumerable AddedAttributesForCreate() - { - yield return new object[] { DicomTag.SOPClassUID }; - yield return new object[] { DicomTag.ScheduledProcedureStepModificationDateTime }; - yield return new object[] { DicomTag.WorklistLabel }; - yield return new object[] { DicomTag.ProcedureStepState }; - } - - [Fact] - internal void GivenQueryParam_AndNoDatasetAttribute_QueryParamIsSet() - { - var dataset = new DicomDataset(); - - WorkitemService.ReconcileWorkitemInstanceUid(dataset, "123"); - - Assert.Equal("123", dataset.GetString(DicomTag.SOPInstanceUID)); - } - - [Fact] - internal void GivenQueryParam_AndMatchingDatasetAttribute_QueryParamIsSet() - { - var dataset = new DicomDataset(new DicomUniqueIdentifier(DicomTag.SOPInstanceUID, "123")); - - WorkitemService.ReconcileWorkitemInstanceUid(dataset, "123"); - - Assert.Equal("123", dataset.GetString(DicomTag.SOPInstanceUID)); - } - - [Fact] - internal void GivenQueryParam_AndDifferentDatasetAttribute_Throws() - { - var dataset = new DicomDataset(new DicomUniqueIdentifier(DicomTag.SOPInstanceUID, "123")); - - Assert.Throws(() => WorkitemService.ReconcileWorkitemInstanceUid(dataset, "456")); - } - - [Fact] - internal void GivenNoQueryParam_AndDatasetAttribute_AttributeIsSet() - { - var dataset = new DicomDataset(new DicomUniqueIdentifier(DicomTag.SOPInstanceUID, "123")); - - WorkitemService.ReconcileWorkitemInstanceUid(dataset, null); - - Assert.Equal("123", dataset.GetString(DicomTag.SOPInstanceUID)); - } - - [Fact] - internal void GivenNoQueryParam_AndNoDatasetAttribute_NothingIsSet() - { - var dataset = new DicomDataset(); - - WorkitemService.ReconcileWorkitemInstanceUid(dataset, null); - - Assert.False(dataset.Contains(DicomTag.SOPInstanceUID)); - } - - [Fact] - public async Task GivenNoWorkitem_ProcessRetrieveAsync_ReturnsNotFoundResponseStatus() - { - var workitemInstanceUid = DicomUID.Generate().UID; - - _orchestrator - .GetWorkitemMetadataAsync(Arg.Is(uid => string.Equals(workitemInstanceUid, uid)), Arg.Any()) - .Returns(Task.FromResult(null as WorkitemMetadataStoreEntry)); - - var response = await _target.ProcessRetrieveAsync(workitemInstanceUid, CancellationToken.None); - - _responseBuilder - .Received() - .AddFailure( - Arg.Is(fc => fc == FailureReasonCodes.UpsInstanceNotFound), - Arg.Is(msg => msg == DicomCoreResource.WorkitemInstanceNotFound)); - } - - private static QueryParameters CreateParameters( - Dictionary filters, - QueryResource resourceType, - string studyInstanceUid = null, - string seriesInstanceUid = null, - bool fuzzyMatching = false, - string[] includeField = null) - { - return new QueryParameters - { - Filters = filters, - FuzzyMatching = fuzzyMatching, - IncludeField = includeField ?? Array.Empty(), - QueryResourceType = resourceType, - SeriesInstanceUid = seriesInstanceUid, - StudyInstanceUid = studyInstanceUid, - }; - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Messages/Delete/DeleteResourcesRequestTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Messages/Delete/DeleteResourcesRequestTests.cs deleted file mode 100644 index b6a607fecd..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Messages/Delete/DeleteResourcesRequestTests.cs +++ /dev/null @@ -1,35 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Microsoft.Health.Dicom.Core.Messages; -using Microsoft.Health.Dicom.Core.Messages.Delete; -using Microsoft.Health.Dicom.Tests.Common; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Messages.Delete; - -public class DeleteResourcesRequestTests -{ - [Fact] - public void GivenDicomDeleteResourcesRequestForStudy_OnConstruction_StudyResourceTypeIsSet() - { - var request = new DeleteResourcesRequest(TestUidGenerator.Generate()); - Assert.Equal(ResourceType.Study, request.ResourceType); - } - - [Fact] - public void GivenDicomDeleteResourcesRequestForSeries_OnConstruction_SeriesResourceTypeIsSet() - { - var request = new DeleteResourcesRequest(TestUidGenerator.Generate(), TestUidGenerator.Generate()); - Assert.Equal(ResourceType.Series, request.ResourceType); - } - - [Fact] - public void GivenDicomDeleteResourcesRequestForInstance_OnConstruction_InstanceResourceTypeIsSet() - { - var request = new DeleteResourcesRequest(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate()); - Assert.Equal(ResourceType.Instance, request.ResourceType); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Messages/ExtendedQueryTag/GetExtendedQueryTagErrorsRequestTest.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Messages/ExtendedQueryTag/GetExtendedQueryTagErrorsRequestTest.cs deleted file mode 100644 index 4d4dbcd4da..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Messages/ExtendedQueryTag/GetExtendedQueryTagErrorsRequestTest.cs +++ /dev/null @@ -1,21 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using Microsoft.Health.Dicom.Core.Messages.ExtendedQueryTag; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Messages.ExtendedQueryTag; - -public class GetExtendedQueryTagErrorsRequestTest -{ - [Fact] - public void GivenInvalidParameters_WhenCreateRequest_ThenThrowArgumentOutOfRangeException() - { - Assert.Throws(() => new GetExtendedQueryTagErrorsRequest("foo", 0, 0)); - Assert.Throws(() => new GetExtendedQueryTagErrorsRequest("bar", 201, 0)); - Assert.Throws(() => new GetExtendedQueryTagErrorsRequest("baz", 10, -12)); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Messages/ExtendedQueryTag/GetExtendedQueryTagsRequestTest.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Messages/ExtendedQueryTag/GetExtendedQueryTagsRequestTest.cs deleted file mode 100644 index af0251fae5..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Messages/ExtendedQueryTag/GetExtendedQueryTagsRequestTest.cs +++ /dev/null @@ -1,21 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using Microsoft.Health.Dicom.Core.Messages.ExtendedQueryTag; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Messages.ExtendedQueryTag; - -public class GetExtendedQueryTagsRequestTest -{ - [Fact] - public void GivenInvalidParameters_WhenCreateRequest_ThenThrowArgumentOutOfRangeException() - { - Assert.Throws(() => new GetExtendedQueryTagsRequest(0, 0)); - Assert.Throws(() => new GetExtendedQueryTagsRequest(201, 0)); - Assert.Throws(() => new GetExtendedQueryTagsRequest(10, -12)); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Messages/Retrieve/AcceptHeaderTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Messages/Retrieve/AcceptHeaderTests.cs deleted file mode 100644 index bd5b905b33..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Messages/Retrieve/AcceptHeaderTests.cs +++ /dev/null @@ -1,43 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using FellowOakDicom; -using Microsoft.Extensions.Primitives; -using Microsoft.Health.Dicom.Core.Features.Retrieve; -using Microsoft.Health.Dicom.Core.Messages.Retrieve; -using Microsoft.Health.Dicom.Core.Web; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Messages.Retrieve; - -public class AcceptHeaderTests -{ - [Fact] - public void GivenValidInput_WhenConstructAcceptHeader_ThenShouldSucceed() - { - StringSegment mediaType = KnownContentTypes.ApplicationDicom; - PayloadTypes payloadType = PayloadTypes.MultipartRelated; - StringSegment transferSytnax = DicomTransferSyntax.ExplicitVRLittleEndian.UID.UID; - double quality = 0.5; - AcceptHeader header = new AcceptHeader(mediaType, payloadType, transferSytnax, quality); - - Assert.Equal(mediaType, header.MediaType); - Assert.Equal(payloadType, header.PayloadType); - Assert.Equal(transferSytnax, header.TransferSyntax); - Assert.Equal(quality, header.Quality); - } - - [Fact] - public void GivenValidInput_WhenToString_ThenShouldReturnExpectedContent() - { - StringSegment mediaType = KnownContentTypes.ApplicationDicom; - PayloadTypes payloadType = PayloadTypes.MultipartRelated; - StringSegment transferSytnax = DicomTransferSyntax.ExplicitVRLittleEndian.UID.UID; - double quality = 0.5; - AcceptHeader header = new AcceptHeader(mediaType, payloadType, transferSytnax, quality); - - Assert.Equal($"MediaType:'{mediaType}', PayloadType:'{payloadType}', TransferSyntax:'{transferSytnax}', Quality:'{quality}'", header.ToString()); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Messages/Retrieve/RetrieveMetadataRequestTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Messages/Retrieve/RetrieveMetadataRequestTests.cs deleted file mode 100644 index f03bd88e82..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Messages/Retrieve/RetrieveMetadataRequestTests.cs +++ /dev/null @@ -1,58 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using Microsoft.Health.Dicom.Core.Messages; -using Microsoft.Health.Dicom.Core.Messages.Retrieve; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Messages.Retrieve; - -public class RetrieveMetadataRequestTests -{ - private readonly Random _random = new Random(); - - [Fact] - public void GivenRetrieveStudyMetadataRequest_WhenConstructed_ThenResourceTypeAndInstanceUidIsSetCorrectly() - { - string studyInstanceUid = Guid.NewGuid().ToString(); - string ifNoneMatch = $"{_random.Next()}-{_random.Next()}"; - var request = new RetrieveMetadataRequest(studyInstanceUid, ifNoneMatch); - - Assert.Equal(ResourceType.Study, request.ResourceType); - Assert.Equal(studyInstanceUid, request.StudyInstanceUid); - Assert.Equal(ifNoneMatch, request.IfNoneMatch); - } - - [Fact] - public void GivenRetrieveSeriesMetadataRequest_WhenConstructed_ThenResourceTypeAndInstanceUidsAreSetCorrectly() - { - string studyInstanceUid = Guid.NewGuid().ToString(); - string seriesInstanceUid = Guid.NewGuid().ToString(); - string ifNoneMatch = $"{_random.Next()}-{_random.Next()}"; - var request = new RetrieveMetadataRequest(studyInstanceUid, seriesInstanceUid, ifNoneMatch); - - Assert.Equal(ResourceType.Series, request.ResourceType); - Assert.Equal(studyInstanceUid, request.StudyInstanceUid); - Assert.Equal(seriesInstanceUid, request.SeriesInstanceUid); - Assert.Equal(ifNoneMatch, request.IfNoneMatch); - } - - [Fact] - public void GivenRetrieveSopInstanceMetadataRequest_WhenConstructed_ThenResourceTypeAndInstanceUidsAreSetCorrectly() - { - string studyInstanceUid = Guid.NewGuid().ToString(); - string seriesInstanceUid = Guid.NewGuid().ToString(); - string sopInstanceUid = Guid.NewGuid().ToString(); - string ifNoneMatch = $"{_random.Next()}-{_random.Next()}"; - var request = new RetrieveMetadataRequest(studyInstanceUid, seriesInstanceUid, sopInstanceUid, ifNoneMatch); - - Assert.Equal(ResourceType.Instance, request.ResourceType); - Assert.Equal(studyInstanceUid, request.StudyInstanceUid); - Assert.Equal(seriesInstanceUid, request.SeriesInstanceUid); - Assert.Equal(sopInstanceUid, request.SopInstanceUid); - Assert.Equal(ifNoneMatch, request.IfNoneMatch); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Messages/Retrieve/RetrieveRenderedRequestsTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Messages/Retrieve/RetrieveRenderedRequestsTests.cs deleted file mode 100644 index c1e18628d9..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Messages/Retrieve/RetrieveRenderedRequestsTests.cs +++ /dev/null @@ -1,45 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using Microsoft.Health.Dicom.Core.Messages; -using Microsoft.Health.Dicom.Core.Messages.Retrieve; -using Microsoft.Health.Dicom.Tests.Common; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Messages.Retrieve; - -public class RetrieveRenderedRequestsTests -{ - [Fact] - public void VerifyAllFieldsSetCorrectlyForInstance() - { - string studyInstanceUid = Guid.NewGuid().ToString(); - string seriesInstanceUid = Guid.NewGuid().ToString(); - string sopInstanceUid = Guid.NewGuid().ToString(); - var request = new RetrieveRenderedRequest(studyInstanceUid, seriesInstanceUid, sopInstanceUid, ResourceType.Instance, 1, 75, new[] { AcceptHeaderHelpers.CreateRenderAcceptHeader() }); - Assert.Equal(studyInstanceUid, request.StudyInstanceUid); - Assert.Equal(seriesInstanceUid, request.SeriesInstanceUid); - Assert.Equal(sopInstanceUid, request.SopInstanceUid); - Assert.Equal(ResourceType.Instance, request.ResourceType); - Assert.Equal(0, request.FrameNumber); - Assert.Equal(75, request.Quality); - } - - [Fact] - public void VerifyAllFieldsSetCorrectlyForFrame() - { - string studyInstanceUid = Guid.NewGuid().ToString(); - string seriesInstanceUid = Guid.NewGuid().ToString(); - string sopInstanceUid = Guid.NewGuid().ToString(); - var request = new RetrieveRenderedRequest(studyInstanceUid, seriesInstanceUid, sopInstanceUid, ResourceType.Frames, 6, 75, new[] { AcceptHeaderHelpers.CreateRenderAcceptHeader() }); - Assert.Equal(studyInstanceUid, request.StudyInstanceUid); - Assert.Equal(seriesInstanceUid, request.SeriesInstanceUid); - Assert.Equal(sopInstanceUid, request.SopInstanceUid); - Assert.Equal(ResourceType.Frames, request.ResourceType); - Assert.Equal(5, request.FrameNumber); - Assert.Equal(75, request.Quality); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Messages/Retrieve/RetrieveRequestValidatorTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Messages/Retrieve/RetrieveRequestValidatorTests.cs deleted file mode 100644 index 46e9bf0e4c..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Messages/Retrieve/RetrieveRequestValidatorTests.cs +++ /dev/null @@ -1,127 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using EnsureThat; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Messages; -using Microsoft.Health.Dicom.Core.Messages.Retrieve; -using Microsoft.Health.Dicom.Tests.Common; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Messages.Retrieve; - -public class RetrieveRequestValidatorTests -{ - [Theory] - [InlineData("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")] - [InlineData("345%^&")] - public void GivenAnInvalidStudyInstanceIdentifier_WhenValidatedForRequestedResourceTypeStudy_ThenInvalidIdentifierExceptionIsThrown(string studyInstanceUid) - { - EnsureArg.IsNotNull(studyInstanceUid, nameof(studyInstanceUid)); - Assert.Throws(() => RetrieveRequestValidator.ValidateInstanceIdentifiers(ResourceType.Study, studyInstanceUid)); - } - - [Theory] - [InlineData("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")] - [InlineData("345%^&")] - [InlineData("aaaa-bbbb")] - [InlineData("()")] - public void GivenAnInvalidSeriesInstanceIdentifier_WhenValidatedForRequestedResourceTypeSeries_ThenInvalidIdentifierExceptionIsThrown(string seriesInstanceUid) - { - EnsureArg.IsNotNull(seriesInstanceUid, nameof(seriesInstanceUid)); - Assert.Throws(() => RetrieveRequestValidator.ValidateInstanceIdentifiers(ResourceType.Series, TestUidGenerator.Generate(), seriesInstanceUid)); - } - - [Theory] - [InlineData("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")] - [InlineData("345%^&")] - [InlineData("aaaa-bbbb")] - [InlineData("()")] - public void GivenAnInvalidInstanceIdentifier_WhenValidatedForRequestedResourceTypeInstance_ThenInvalidIdentifierExceptionIsThrown(string sopInstanceUid) - { - EnsureArg.IsNotNull(sopInstanceUid, nameof(sopInstanceUid)); - Assert.Throws(() => RetrieveRequestValidator.ValidateInstanceIdentifiers(ResourceType.Instance, TestUidGenerator.Generate(), TestUidGenerator.Generate(), sopInstanceUid)); - } - - [Theory] - [InlineData("1", "1", "2")] - [InlineData("1", "2", "1")] - [InlineData("1", "2", "2")] - public void GivenARequestWithRepeatedIdentifiers_WhenValidatedForRequestedResourceTypeInstance_ThenNoExceptionIsThrown(string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid) - { - RetrieveRequestValidator.ValidateInstanceIdentifiers(ResourceType.Instance, studyInstanceUid, seriesInstanceUid, sopInstanceUid); - } - - [Fact] - public void GivenARequestWithRepeatedStudyAndSeriesInstanceIdentifiers_WhenValidatedForRequestedResourceTypeSeries_ThenNoExceptionIsThrown() - { - string studyInstanceUid = TestUidGenerator.Generate(); - - // Use same identifier as studyInstanceUid and seriesInstanceUid. - RetrieveRequestValidator.ValidateInstanceIdentifiers(ResourceType.Series, studyInstanceUid, studyInstanceUid); - } - - [Theory] - [InlineData("*-")] - [InlineData("invalid")] - [InlineData("00000000000000000000000000000000000000000000000000000000000000065")] - public void GivenARequestWithIncorrectTransferSyntax_WhenValidated_ThenBadRequestExceptionIsThrown(string transferSyntax) - { - var ex = Assert.Throws(() => RetrieveRequestValidator.ValidateTransferSyntax(requestedTransferSyntax: transferSyntax)); - Assert.Equal("The specified Transfer Syntax value is not valid.", ex.Message); - } - - [Theory] - [InlineData(null)] - [InlineData(new int[0])] - public void GivenARequestWithNoFrames_WhenValidated_ThenBadRequestExceptionIsThrown(int[] frames) - { - string expectedErrorMessage = "The specified frames value is not valid. At least one frame must be present, and all requested frames must have value greater than 0."; - - var ex = Assert.Throws(() => RetrieveRequestValidator.ValidateFrames(frames)); - Assert.Equal(expectedErrorMessage, ex.Message); - } - - [Theory] - [InlineData(-1)] - [InlineData(-234)] - public void GivenARequestWithInvalidFrameNumber_WhenValidated_ThenBadRequestExceptionIsThrown(int frame) - { - string expectedErrorMessage = "The specified frames value is not valid. At least one frame must be present, and all requested frames must have value greater than 0."; - - var ex = Assert.Throws(() => RetrieveRequestValidator.ValidateFrames(new[] { frame })); - - Assert.Equal(expectedErrorMessage, ex.Message); - } - - [Theory] - [InlineData(ResourceType.Study)] - [InlineData(ResourceType.Series)] - [InlineData(ResourceType.Instance)] - public void GivenARequestWithValidInstanceIdentifiers__WhenValidatedForSpecifiedResourceType_ThenNoExceptionIsThrown(ResourceType resourceType) - { - RetrieveRequestValidator.ValidateInstanceIdentifiers(resourceType, TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate()); - } - - [Fact] - public void GivenARequestWithValidFramesValue_WhenValidated_ThenNoExceptionIsThrown() - { - RetrieveRequestValidator.ValidateFrames(new List { 1 }); - } - - [Fact] - public void GivenARequestWithValidTransferSyntax_WhenValidated_ThenNoExceptionIsThrown() - { - RetrieveRequestValidator.ValidateTransferSyntax(DicomTransferSyntax.ExplicitVRLittleEndian.UID.UID); - } - - [Fact] - public void GivenARequestWithOriginalTransferSyntax_WhenValidated_ThenNoExceptionIsThrown() - { - RetrieveRequestValidator.ValidateTransferSyntax(requestedTransferSyntax: "*", originalTransferSyntaxRequested: true); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Messages/Retrieve/RetrieveResourceRequestTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Messages/Retrieve/RetrieveResourceRequestTests.cs deleted file mode 100644 index cfa493d636..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Messages/Retrieve/RetrieveResourceRequestTests.cs +++ /dev/null @@ -1,54 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Microsoft.Health.Dicom.Core.Messages; -using Microsoft.Health.Dicom.Core.Messages.Retrieve; -using Microsoft.Health.Dicom.Tests.Common; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Messages.Retrieve; - -public class RetrieveResourceRequestTests -{ - [Fact] - public void GivenRetrieveResourcesRequestForStudy_WhenConstructed_ThenStudyResourceTypeIsSet() - { - var request = new RetrieveResourceRequest(TestUidGenerator.Generate(), new[] { AcceptHeaderHelpers.CreateAcceptHeaderForGetInstance(transferSyntax: string.Empty) }); - Assert.Equal(ResourceType.Study, request.ResourceType); - } - - [Fact] - public void GivenRetrieveResourcesRequestForSeries_WhenConstructed_ThenSeriesResourceTypeIsSet() - { - var request = new RetrieveResourceRequest( - TestUidGenerator.Generate(), - TestUidGenerator.Generate(), - new[] { AcceptHeaderHelpers.CreateAcceptHeaderForGetSeries(transferSyntax: string.Empty) }); - Assert.Equal(ResourceType.Series, request.ResourceType); - } - - [Fact] - public void GivenRetrieveResourcesRequestForInstance_WhenConstructed_ThenInstanceResourceTypeIsSet() - { - var request = new RetrieveResourceRequest( - TestUidGenerator.Generate(), - TestUidGenerator.Generate(), - TestUidGenerator.Generate(), - new[] { AcceptHeaderHelpers.CreateAcceptHeaderForGetInstance(transferSyntax: string.Empty) }); - Assert.Equal(ResourceType.Instance, request.ResourceType); - } - - [Fact] - public void GivenRetrieveResourcesRequestForFrames_WhenConstructed_ThenFramesResourceTypeIsSet() - { - var request = new RetrieveResourceRequest( - TestUidGenerator.Generate(), - TestUidGenerator.Generate(), - TestUidGenerator.Generate(), - new[] { 5 }, - new[] { AcceptHeaderHelpers.CreateAcceptHeaderForGetFrame(transferSyntax: string.Empty) }); - Assert.Equal(ResourceType.Frames, request.ResourceType); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Microsoft.Health.Dicom.Core.UnitTests.csproj b/src/Microsoft.Health.Dicom.Core.UnitTests/Microsoft.Health.Dicom.Core.UnitTests.csproj deleted file mode 100644 index dce84481b2..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Microsoft.Health.Dicom.Core.UnitTests.csproj +++ /dev/null @@ -1,58 +0,0 @@ - - - - $(LibraryFrameworks) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - True - True - Resource.resx - - - - - - ResXFileCodeGenerator - Resource.Designer.cs - - - - diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/MockMessageHandler.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/MockMessageHandler.cs deleted file mode 100644 index 80022df21c..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/MockMessageHandler.cs +++ /dev/null @@ -1,32 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.Health.Dicom.Core.UnitTests; - -internal sealed class MockMessageHandler : HttpMessageHandler -{ - public event Action Sending; - - public int SentMessages { get; private set; } - - private readonly HttpResponseMessage _expected; - - public MockMessageHandler(HttpResponseMessage expected) - { - _expected = expected; - } - - protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - Sending?.Invoke(request, cancellationToken); - SentMessages++; - return Task.FromResult(_expected); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Models/DicomIdentifierTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Models/DicomIdentifierTests.cs deleted file mode 100644 index ad217e8e45..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Models/DicomIdentifierTests.cs +++ /dev/null @@ -1,41 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - - -using Microsoft.Health.Dicom.Core.Messages; -using Microsoft.Health.Dicom.Core.Models.Common; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Models; - -public class DicomIdentifierTests -{ - [Theory] - [InlineData("1.2.345", null, null, ResourceType.Study)] - [InlineData("1.2.345", "67.89", null, ResourceType.Series)] - [InlineData("1.2.345", "67.89", "10.11121314.1516.17.18.1920", ResourceType.Instance)] - public void GivenDicomIdentifier_WhenQueryingType_ThenMapType(string study, string series, string instance, ResourceType expected) - => Assert.Equal(expected, new DicomIdentifier(study, series, instance).Type); - - [Theory] - [InlineData("1.2.345", null, null, "1.2.345")] - [InlineData("1.2.345", "67.89", null, "1.2.345/67.89")] - [InlineData("1.2.345", "67.89", "10.11121314.1516.17.18.1920", "1.2.345/67.89/10.11121314.1516.17.18.1920")] - public void GivenValidString_WhenParsing_ThenGetDicomIdentifier(string study, string series, string instance, string value) - { - var actual = DicomIdentifier.Parse(value); - - Assert.Equal(study, actual.StudyInstanceUid); - Assert.Equal(series, actual.SeriesInstanceUid); - Assert.Equal(instance, actual.SopInstanceUid); - } - - [Theory] - [InlineData("1.2.345", null, null, "1.2.345")] - [InlineData("1.2.345", "67.89", null, "1.2.345/67.89")] - [InlineData("1.2.345", "67.89", "10.11121314.1516.17.18.1920", "1.2.345/67.89/10.11121314.1516.17.18.1920")] - public void GivenDicomIdentifier_WhenConvertingToString_ThenGetString(string study, string series, string instance, string expected) - => Assert.Equal(expected, new DicomIdentifier(study, series, instance).ToString()); -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Models/Export/IdentifierExportOptionsTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Models/Export/IdentifierExportOptionsTests.cs deleted file mode 100644 index 89ef3c2b53..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Models/Export/IdentifierExportOptionsTests.cs +++ /dev/null @@ -1,38 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using Microsoft.Health.Dicom.Core.Models.Common; -using Microsoft.Health.Dicom.Core.Models.Export; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Models.Export; - -public class IdentifierExportOptionsTests -{ - [Fact] - public void GivenInvalidOptions_WhenValidating_ThenReturnResults() - { - Assert.Single(new IdentifierExportOptions { Values = null }.Validate(null)); - Assert.Single(new IdentifierExportOptions { Values = Array.Empty() }.Validate(null)); - } - - [Fact] - public void GivenValidOptions_WhenValidating_ThenReturnNoFailures() - { - var options = new IdentifierExportOptions - { - Values = new List - { - DicomIdentifier.ForInstance("1.2", "3.4.5", "6.7.8.10"), - DicomIdentifier.ForSeries("11.12.13", "14"), - DicomIdentifier.ForStudy("1516.17"), - }, - }; - - Assert.Empty(options.Validate(null)); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Resource.Designer.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Resource.Designer.cs deleted file mode 100644 index 68caef6ad2..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Resource.Designer.cs +++ /dev/null @@ -1,93 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Microsoft.Health.Dicom.Core.UnitTests { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resource { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resource() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.Health.Dicom.Core.UnitTests.Resource", typeof(Resource).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized resource of type System.Byte[]. - /// - internal static byte[] case1_008 { - get { - object obj = ResourceManager.GetObject("case1_008", resourceCulture); - return ((byte[])(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Byte[]. - /// - internal static byte[] layer1 { - get { - object obj = ResourceManager.GetObject("layer1", resourceCulture); - return ((byte[])(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Byte[]. - /// - internal static byte[] red_triangle { - get { - object obj = ResourceManager.GetObject("red_triangle", resourceCulture); - return ((byte[])(obj)); - } - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Resource.resx b/src/Microsoft.Health.Dicom.Core.UnitTests/Resource.resx deleted file mode 100644 index 6d319aedc5..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Resource.resx +++ /dev/null @@ -1,130 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - - Resources\case1_008.dcm;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Resources\layer1.dcm;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Resources\red-triangle.dcm;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - \ No newline at end of file diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Resources/case1_008.dcm b/src/Microsoft.Health.Dicom.Core.UnitTests/Resources/case1_008.dcm deleted file mode 100644 index dd36ab22c9..0000000000 Binary files a/src/Microsoft.Health.Dicom.Core.UnitTests/Resources/case1_008.dcm and /dev/null differ diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Resources/layer1.dcm b/src/Microsoft.Health.Dicom.Core.UnitTests/Resources/layer1.dcm deleted file mode 100644 index 1462fb2c24..0000000000 Binary files a/src/Microsoft.Health.Dicom.Core.UnitTests/Resources/layer1.dcm and /dev/null differ diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Resources/red-triangle.dcm b/src/Microsoft.Health.Dicom.Core.UnitTests/Resources/red-triangle.dcm deleted file mode 100644 index b9cbe05269..0000000000 Binary files a/src/Microsoft.Health.Dicom.Core.UnitTests/Resources/red-triangle.dcm and /dev/null differ diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Serialization/DicomIdentifierJsonConverterTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Serialization/DicomIdentifierJsonConverterTests.cs deleted file mode 100644 index 4535fe7d40..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Serialization/DicomIdentifierJsonConverterTests.cs +++ /dev/null @@ -1,48 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Text.Json; -using Microsoft.Health.Dicom.Core.Models.Common; -using Microsoft.Health.Dicom.Core.Serialization; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Serialization; - -public class DicomIdentifierJsonConverterTests -{ - private readonly JsonSerializerOptions _serializerOptions; - - public DicomIdentifierJsonConverterTests() - { - _serializerOptions = new JsonSerializerOptions(); - _serializerOptions.Converters.Add(new DicomIdentifierJsonConverter()); - } - - [Fact] - public void GivenInvalidToken_WhenReading_ThenThrow() - { - Assert.Throws(() => JsonSerializer.Deserialize("123", _serializerOptions)); - } - - [Theory] - [InlineData("\"1.2.345\"", "1.2.345", null, null)] - [InlineData("\"1.2.345/67.89\"", "1.2.345", "67.89", null)] - [InlineData("\"1.2.345/67.89/10.11121314.1516.17.18.1920\"", "1.2.345", "67.89", "10.11121314.1516.17.18.1920")] - public void GivenJson_WhenReading_ThenDeserialize(string json, string study, string series, string instance) - { - DicomIdentifier actual = JsonSerializer.Deserialize(json, _serializerOptions); - - Assert.Equal(study, actual.StudyInstanceUid); - Assert.Equal(series, actual.SeriesInstanceUid); - Assert.Equal(instance, actual.SopInstanceUid); - } - - [Theory] - [InlineData("1.2.345", null, null, "\"1.2.345\"")] - [InlineData("1.2.345", "67.89", null, "\"1.2.345/67.89\"")] - [InlineData("1.2.345", "67.89", "10.11121314.1516.17.18.1920", "\"1.2.345/67.89/10.11121314.1516.17.18.1920\"")] - public void GivenDicomIdentifier_WhenConvertingToString_ThenGetString(string study, string series, string instance, string expected) - => Assert.Equal(expected, JsonSerializer.Serialize(new DicomIdentifier(study, series, instance), _serializerOptions)); -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Serialization/ExportDataOptionsJsonConverterTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Serialization/ExportDataOptionsJsonConverterTests.cs deleted file mode 100644 index de9f376fcf..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Serialization/ExportDataOptionsJsonConverterTests.cs +++ /dev/null @@ -1,259 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.IO; -using System.Linq; -using System.Text.Json; -using System.Text.Json.Serialization; -using Microsoft.Health.Dicom.Core.Models; -using Microsoft.Health.Dicom.Core.Models.Common; -using Microsoft.Health.Dicom.Core.Models.Export; -using Microsoft.Health.Dicom.Core.Serialization; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Serialization; - -public class ExportDataOptionsJsonConverterTests -{ - private readonly JsonSerializerOptions _serializerOptions; - - public ExportDataOptionsJsonConverterTests() - { - _serializerOptions = new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - }; - _serializerOptions.Converters.Add(new DicomIdentifierJsonConverter()); - _serializerOptions.Converters.Add(new ExportDataOptionsJsonConverter()); - _serializerOptions.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)); - } - - [Fact] - public void GivenInvalidTypeArgument_WhenCreatingConverter_ThenThrow() - { - Assert.Throws(() => JsonSerializer.Deserialize>("", _serializerOptions)); - } - - [Theory] - [InlineData("foo")] - [InlineData("unknown")] - public void GivenInvalidSouceType_WhenReading_ThenThrow(string type) - { - string json = @$"{{ - ""type"": ""{type}"", - ""settings"": {{ - ""values"": [ - ""1.2"", - ""3.4.5/67"", - ""8.9.10/1.112.13/1.4"" - ] - }} -}}"; - - Assert.Throws(() => JsonSerializer.Deserialize>(json, _serializerOptions)); - } - - [Theory] - [InlineData("bar")] - [InlineData("unknown")] - public void GivenInvalidDestinationType_WhenReading_ThenThrow(string type) - { - string json = @$"{{ - ""type"": ""{type}"", - ""settings"": {{ - ""values"": [ - ""1.2"", - ""3.4.5/67"", - ""8.9.10/1.112.13/1.4"" - ] - }} -}}"; - - Assert.Throws(() => JsonSerializer.Deserialize>(json, _serializerOptions)); - } - - [Fact] - public void GivenInvalidSourceOptionsJson_WhenReading_ThenDeserialize() - { - const string json = @"{ - ""type"": ""identifiers"", - ""settings"": { - ""flag"": true, - ""hello"": [ - ""w"", - ""o"", - ""r"", - ""l"", - ""d"" - ] - } -}"; - - ExportDataOptions actual = JsonSerializer.Deserialize>(json, _serializerOptions); - Assert.Equal(ExportSourceType.Identifiers, actual.Type); - - var options = actual.Settings as IdentifierExportOptions; - Assert.Null(options.Values); - } - - [Fact] - public void GivenInvalidDestinationOptionsJson_WhenReading_ThenDeserialize() - { - const string json = @"{ - ""type"": ""azureblob"", - ""settings"": { - ""connnectionStrong"":""BlobEndpoint=https://unit-test.blob.core.windows.net/;Foo=Bar"", - ""blobContainerNamee"": ""mycontainer"" - } -}"; - - ExportDataOptions actual = JsonSerializer.Deserialize>(json, _serializerOptions); - Assert.Equal(ExportDestinationType.AzureBlob, actual.Type); - - var options = actual.Settings as AzureBlobExportOptions; - Assert.Null(options.ConnectionString); - Assert.Null(options.BlobContainerName); - Assert.Null(options.BlobContainerUri); - } - - [Fact] - public void GivenSourceOptionsJson_WhenReading_ThenDeserialize() - { - const string json = @"{ - ""type"": ""identifiers"", - ""settings"": { - ""values"": [ - ""1.2"", - ""3.4.5/67"", - ""8.9.10/1.112.13/1.4"" - ] - } -}"; - - ExportDataOptions actual = JsonSerializer.Deserialize>(json, _serializerOptions); - Assert.Equal(ExportSourceType.Identifiers, actual.Type); - - var options = actual.Settings as IdentifierExportOptions; - Assert.Equal(3, options.Values.Count); - Assert.True(options.Values.SequenceEqual( - new DicomIdentifier[] - { - DicomIdentifier.ForStudy("1.2"), - DicomIdentifier.ForSeries("3.4.5", "67"), - DicomIdentifier.ForInstance("8.9.10", "1.112.13", "1.4"), - })); - } - - [Fact] - public void GivenDestinationOptionsJson_WhenReading_ThenDeserialize() - { - const string json = @"{ - ""type"": ""azureblob"", - ""settings"": { - ""connectionString"": ""BlobEndpoint=https://unit-test.blob.core.windows.net/;Foo=Bar"", - ""blobContainerName"": ""mycontainer"", - ""blobContainerUri"": ""https://unit-test.blob.core.windows.net/mycontainer"", - ""secret"": { - ""name"": ""foo"", - ""version"": ""1"" - }, - ""useManagedIdentity"": true - } -}"; - - ExportDataOptions actual = JsonSerializer.Deserialize>(json, _serializerOptions); - Assert.Equal(ExportDestinationType.AzureBlob, actual.Type); - - var options = actual.Settings as AzureBlobExportOptions; - Assert.Equal("BlobEndpoint=https://unit-test.blob.core.windows.net/;Foo=Bar", options.ConnectionString); - Assert.Equal("mycontainer", options.BlobContainerName); - Assert.Equal(new Uri("https://unit-test.blob.core.windows.net/mycontainer"), options.BlobContainerUri); - Assert.Null(options.Secret); - Assert.True(options.UseManagedIdentity); - } - - [Theory] - [InlineData((ExportSourceType)12)] - [InlineData(ExportSourceType.Unknown)] - public void GivenInvalidSouceType_WhenWriting_ThenThrow(ExportSourceType type) - { - Assert.Throws( - () => JsonSerializer.Serialize( - new ExportDataOptions(type, new object()), - _serializerOptions)); - } - - [Theory] - [InlineData((ExportDestinationType)12)] - [InlineData(ExportDestinationType.Unknown)] - public void GivenInvalidDestinationType_WhenWriting_ThenThrow(ExportDestinationType type) - { - Assert.Throws( - () => JsonSerializer.Serialize( - new ExportDataOptions(type, new object()), - _serializerOptions)); - } - - [Fact] - public void GivenSourceOptions_WhenWriting_ThenSerialize() - { - var expected = new ExportDataOptions( - ExportSourceType.Identifiers, - new IdentifierExportOptions - { - Values = new DicomIdentifier[] - { - DicomIdentifier.ForStudy("1.2"), - DicomIdentifier.ForSeries("3.4.5", "67"), - DicomIdentifier.ForInstance("8.9.10", "1.112.13", "1.4"), - } - }); - - string actual = JsonSerializer.Serialize(expected, _serializerOptions); - Assert.Equal( -@"{ - ""type"": ""identifiers"", - ""settings"": { - ""values"": [ - ""1.2"", - ""3.4.5/67"", - ""8.9.10/1.112.13/1.4"" - ] - } -}", - actual); - } - - [Fact] - public void GivenDestinationOptions_WhenWriting_ThenSerialize() - { - var expected = new ExportDataOptions( - ExportDestinationType.AzureBlob, - new AzureBlobExportOptions - { - ConnectionString = "BlobEndpoint=https://unit-test.blob.core.windows.net/;Foo=Bar", - BlobContainerName = "mycontainer", - BlobContainerUri = new Uri("https://unit-test.blob.core.windows.net/mycontainer"), - Secret = new SecretKey { Name = "foo", Version = "1" }, - UseManagedIdentity = true, - }); - - string actual = JsonSerializer.Serialize(expected, _serializerOptions); - Assert.Equal( -@"{ - ""type"": ""azureBlob"", - ""settings"": { - ""blobContainerUri"": ""https://unit-test.blob.core.windows.net/mycontainer"", - ""connectionString"": ""BlobEndpoint=https://unit-test.blob.core.windows.net/;Foo=Bar"", - ""blobContainerName"": ""mycontainer"", - ""useManagedIdentity"": true - } -}", - actual); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Serialization/JsonDicomConverterExtendedTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Serialization/JsonDicomConverterExtendedTests.cs deleted file mode 100644 index 9e97aa2a14..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Serialization/JsonDicomConverterExtendedTests.cs +++ /dev/null @@ -1,331 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Linq; -using System.Text; -using System.Text.Json; -using FellowOakDicom; -using FellowOakDicom.IO.Buffer; -using Microsoft.Health.FellowOakDicom.Serialization; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Serialization; - -public class JsonDicomConverterExtendedTests -{ - private static readonly JsonSerializerOptions SerializerOptions = new JsonSerializerOptions(); - - static JsonDicomConverterExtendedTests() - { - SerializerOptions.Converters.Add(new DicomJsonConverter(writeTagsAsKeywords: false, autoValidate: false)); - } - - [Fact] - public static void GivenDatasetWithEscapedCharacters_WhenSerialized_IsDeserializedCorrectly() - { - var unlimitedTextValue = "Multi\nLine\ttab\"quoted\"formfeed\f"; - - var dicomDataset = new DicomDataset - { - { DicomTag.StrainAdditionalInformation, unlimitedTextValue }, - }; - - var json = JsonSerializer.Serialize(dicomDataset, SerializerOptions); - JsonDocument.Parse(json); - DicomDataset deserializedDataset = JsonSerializer.Deserialize(json, SerializerOptions); - var recoveredString = deserializedDataset.GetValue(DicomTag.StrainAdditionalInformation, 0); - Assert.Equal(unlimitedTextValue, recoveredString); - } - - [Fact] - public static void GivenDatasetWithUnicodeCharacters_WhenSerialized_IsDeserializedCorrectly() - { - var unlimitedTextValue = "⚽"; - - var dicomDataset = new DicomDataset { { DicomTag.StrainAdditionalInformation, unlimitedTextValue }, }; - - var json = JsonSerializer.Serialize(dicomDataset, SerializerOptions); - JsonDocument.Parse(json); - DicomDataset deserializedDataset = JsonSerializer.Deserialize(json, SerializerOptions); - var recoveredString = deserializedDataset.GetValue(DicomTag.StrainAdditionalInformation, 0); - Assert.Equal(unlimitedTextValue, recoveredString); - } - - [Fact] - public static void GivenDicomDatasetWithBase64EncodedPixelData_WhenSerialized_IsDeserializedCorrectly() - { - var pixelData = Enumerable.Range(0, 1 << 8).Select(v => (byte)v).ToArray(); - var dicomDataset = new DicomDataset - { - { DicomTag.PixelData, pixelData }, - }; - - var json = JsonSerializer.Serialize(dicomDataset, SerializerOptions); - JsonDocument.Parse(json); - DicomDataset deserializedDataset = JsonSerializer.Deserialize(json, SerializerOptions); - var recoveredPixelData = deserializedDataset.GetValues(DicomTag.PixelData); - Assert.Equal(pixelData, recoveredPixelData); - } - - [Fact] - public static void GivenOWDicomDatasetWithBase64EncodedPixelData_WhenSerialized_IsDeserializedCorrectly() - { - var pixelData = Enumerable.Range(0, 1 << 16).Select(v => (ushort)v).ToArray(); - var dicomDataset = new DicomDataset - { - new DicomOtherWord(DicomTag.PixelData, pixelData), - }; - - var json = JsonSerializer.Serialize(dicomDataset, SerializerOptions); - JsonDocument.Parse(json); - DicomDataset deserializedDataset = JsonSerializer.Deserialize(json, SerializerOptions); - var recoveredPixelData = deserializedDataset.GetValues(DicomTag.PixelData); - Assert.Equal(pixelData, recoveredPixelData); - } - - [Fact] - public static void GivenInvalidDicomJsonDataset_WhenDeserialized_JsonReaderExceptionIsThrown() - { - const string json = @" - { - ""00081030"": { - ""VR"": ""LO"", - ""Value"": [ ""Study1"" ] - } - } - "; - Assert.Throws(() => JsonSerializer.Deserialize(json, SerializerOptions)); - } - - [Fact] - public static void GivenDicomJsonDatasetWithInvalidVR_WhenDeserialized_NotSupportedExceptionIsThrown() - { - const string json = @" - { - ""00081030"": { - ""vr"": ""BADVR"", - ""Value"": [ ""Study1"" ] - } - } - "; - Assert.Throws(() => JsonSerializer.Deserialize(json, SerializerOptions)); - } - - [Fact] - public static void GivenDicomJsonDatasetWithInvalidNumberVR_WhenDeserializedWithAutoValidateTrue_NumberExpectedJsonExceptionIsThrown() - { - const string json = @" - { - ""00081030"": { - ""vr"": ""IS"", - ""Value"": [ ""01:02:03"" ] - } - } - "; - - var serializerOptions = new JsonSerializerOptions - { - Converters = { new DicomJsonConverter(writeTagsAsKeywords: false, autoValidate: true) } - }; - - Assert.Throws(() => JsonSerializer.Deserialize(json, serializerOptions)); - } - - [Fact] - public static void GivenDicomJsonDatasetWithFloatingVRContainsNAN_WhenDeserialized_IsSuccessful() - { - const string json = @" - { - ""00720076"": { - ""vr"": ""FL"", - ""Value"": [""NaN""] - } - } "; - - DicomDataset tagValue = JsonSerializer.Deserialize(json, SerializerOptions); - Assert.NotNull(tagValue.GetDicomItem(DicomTag.SelectorFLValue)); - } - - - [Fact] - public void DeserializeDSWithNonNumericValueAsStringDoesNotThrowException() - { - // in DICOM Standard PS3.18 F.2.3.1 now VRs DS, IS SV and UV may be either number or string - var json = @" - { - ""00101030"": { - ""vr"":""DS"", - ""Value"":[84.5] - }, - ""00101020"": { - ""vr"":""DS"", - ""Value"":[""asd""] - } - - }"; - - var serializerOptions = new JsonSerializerOptions - { - Converters = - { - new DicomJsonConverter(autoValidate: false, numberSerializationMode: NumberSerializationMode.PreferablyAsNumber) - } - }; - - var dataset = JsonSerializer.Deserialize(json, serializerOptions); - Assert.NotNull(dataset); - Assert.Equal(84.5m, dataset.GetSingleValue(DicomTag.PatientWeight)); - Assert.Equal(@"asd", dataset.GetString(DicomTag.PatientSize)); - } - - [Fact] - public void DeserializeISWithNonNumericValueAsStringDoesNotThrowException() - { - // in DICOM Standard PS3.18 F.2.3.1 now VRs DS, IS SV and UV may be either number or string - var json = @" - { - ""00201206"": { - ""vr"":""IS"", - ""Value"":[311] - }, - ""00201209"": { - ""vr"":""IS"", - ""Value"":[""asd""] - }, - ""00201204"": { - ""vr"":""IS"", - ""Value"":[] - } - }"; - var serializerOptions = new JsonSerializerOptions - { - Converters = - { - new DicomJsonConverter(autoValidate: false, numberSerializationMode: NumberSerializationMode.PreferablyAsNumber) - } - }; - - var dataset = JsonSerializer.Deserialize(json, serializerOptions); - - Assert.NotNull(dataset); - Assert.Equal(311, dataset.GetSingleValue(DicomTag.NumberOfStudyRelatedSeries)); - Assert.Equal(@"asd", dataset.GetString(DicomTag.NumberOfSeriesRelatedInstances)); - } - - - [Fact] - public void DeserializeSVWithNonNumericValueAsStringDoesNotThrowException() - { - // in DICOM Standard PS3.18 F.2.3.1 now VRs DS, IS SV and UV may be either number or string - var json = @" - { - ""00101030"": { - ""vr"":""SV"", - ""Value"":[84] - }, - ""00101020"": { - ""vr"":""SV"", - ""Value"":[""asd""] - } - - }"; - var serializerOptions = new JsonSerializerOptions - { - Converters = - { - new DicomJsonConverter(autoValidate: false, numberSerializationMode: NumberSerializationMode.PreferablyAsNumber) - } - }; - - var dataset = JsonSerializer.Deserialize(json, serializerOptions); - - Assert.NotNull(dataset); - Assert.Equal(84, dataset.GetSingleValue(DicomTag.PatientWeight)); - Assert.Equal(@"asd", dataset.GetString(DicomTag.PatientSize)); - } - - - [Fact] - public void DeserializeUVWithNonNumericValueAsStringDoesNotThrowException() - { - // in DICOM Standard PS3.18 F.2.3.1 now VRs DS, IS SV and UV may be either number or string - var json = @" - { - ""00101030"": { - ""vr"":""UV"", - ""Value"":[84] - }, - ""00101020"": { - ""vr"":""UV"", - ""Value"":[""asd""] - } - - }"; - var serializerOptions = new JsonSerializerOptions - { - Converters = - { - new DicomJsonConverter(autoValidate: false, numberSerializationMode: NumberSerializationMode.PreferablyAsNumber) - } - }; - - var dataset = JsonSerializer.Deserialize(json, serializerOptions); - - Assert.NotNull(dataset); - Assert.Equal(84ul, dataset.GetSingleValue(DicomTag.PatientWeight)); - Assert.Equal(@"asd", dataset.GetString(DicomTag.PatientSize)); - } - - [Fact] - public static void GivenDicomJsonDatasetWithInvalidPrivateCreatorDataElement_WhenDeserialized_IsSuccessful() - { - // allowing deserializer to handle bad data more gracefully - const string json = @" - { - ""00090010"": { - ""vr"": ""US"", - ""Value"": [ - 1234, - 3333 - ] - }, - ""00091001"": { - ""vr"": ""CS"", - ""Value"": [ - ""00"" - ] - } - } "; - - // make sure below serialization does not throw - DicomDataset ds = JsonSerializer.Deserialize(json, SerializerOptions); - Assert.NotNull(ds); - } - - [Theory] - [InlineData("2147384638123")] - [InlineData("73.8")] - [InlineData("InvalidNumber")] - public static void GivenDatasetWithInvalidOrOverflowNumberForValueRepresentationIS_WhenSerialized_IsDeserializedCorrectly(string overflowNumber) - { - var dicomDataset = new DicomDataset().NotValidated(); - dicomDataset.Add(new DicomIntegerString(DicomTag.Exposure, new MemoryByteBuffer(Encoding.ASCII.GetBytes(overflowNumber)))); - - var serializerOptions = new JsonSerializerOptions - { - Converters = - { - new DicomJsonConverter(autoValidate: false, numberSerializationMode: NumberSerializationMode.PreferablyAsNumber) - } - }; - - var json = JsonSerializer.Serialize(dicomDataset, serializerOptions); - JsonDocument.Parse(json); - DicomDataset deserializedDataset = JsonSerializer.Deserialize(json, serializerOptions); - var recoveredString = deserializedDataset.GetValue(DicomTag.Exposure, 0); - Assert.Equal(overflowNumber, recoveredString); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Serialization/Newtonsoft/DicomIdentifierJsonConverterTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Serialization/Newtonsoft/DicomIdentifierJsonConverterTests.cs deleted file mode 100644 index 2febedd9a7..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Serialization/Newtonsoft/DicomIdentifierJsonConverterTests.cs +++ /dev/null @@ -1,48 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Microsoft.Health.Dicom.Core.Models.Common; -using Microsoft.Health.Dicom.Core.Serialization.Newtonsoft; -using Newtonsoft.Json; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Serialization.Newtonsoft; - -public class DicomIdentifierJsonConverterTests -{ - private readonly JsonSerializerSettings _serializerSettings; - - public DicomIdentifierJsonConverterTests() - { - _serializerSettings = new JsonSerializerSettings(); - _serializerSettings.Converters.Add(new DicomIdentifierJsonConverter()); - } - - [Fact] - public void GivenInvalidToken_WhenReading_ThenThrow() - { - Assert.Throws(() => JsonConvert.DeserializeObject("123", _serializerSettings)); - } - - [Theory] - [InlineData("\"1.2.345\"", "1.2.345", null, null)] - [InlineData("\"1.2.345/67.89\"", "1.2.345", "67.89", null)] - [InlineData("\"1.2.345/67.89/10.11121314.1516.17.18.1920\"", "1.2.345", "67.89", "10.11121314.1516.17.18.1920")] - public void GivenJson_WhenReading_ThenDeserialize(string json, string study, string series, string instance) - { - DicomIdentifier actual = JsonConvert.DeserializeObject(json, _serializerSettings); - - Assert.Equal(study, actual.StudyInstanceUid); - Assert.Equal(series, actual.SeriesInstanceUid); - Assert.Equal(instance, actual.SopInstanceUid); - } - - [Theory] - [InlineData("1.2.345", null, null, "\"1.2.345\"")] - [InlineData("1.2.345", "67.89", null, "\"1.2.345/67.89\"")] - [InlineData("1.2.345", "67.89", "10.11121314.1516.17.18.1920", "\"1.2.345/67.89/10.11121314.1516.17.18.1920\"")] - public void GivenDicomIdentifier_WhenConvertingToString_ThenGetString(string study, string series, string instance, string expected) - => Assert.Equal(expected, JsonConvert.SerializeObject(new DicomIdentifier(study, series, instance), _serializerSettings)); -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Serialization/Newtonsoft/ExportDataOptionsJsonConverterTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Serialization/Newtonsoft/ExportDataOptionsJsonConverterTests.cs deleted file mode 100644 index 487f427ad0..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Serialization/Newtonsoft/ExportDataOptionsJsonConverterTests.cs +++ /dev/null @@ -1,217 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Linq; -using Microsoft.Health.Dicom.Core.Models; -using Microsoft.Health.Dicom.Core.Models.Common; -using Microsoft.Health.Dicom.Core.Models.Export; -using Microsoft.Health.Dicom.Core.Serialization.Newtonsoft; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Newtonsoft.Json.Serialization; -using Xunit; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Serialization.Newtonsoft; - -public class ExportDataOptionsJsonConverterTests -{ - private readonly JsonSerializerSettings _serializeSettings; - - public ExportDataOptionsJsonConverterTests() - { - NamingStrategy camelCase = new CamelCaseNamingStrategy(); - - _serializeSettings = new JsonSerializerSettings - { - ContractResolver = new DefaultContractResolver { NamingStrategy = camelCase }, - Formatting = Formatting.Indented, - }; - _serializeSettings.Converters.Add(new DicomIdentifierJsonConverter()); - _serializeSettings.Converters.Add(new ExportSourceOptionsJsonConverter(camelCase)); - _serializeSettings.Converters.Add(new ExportDestinationOptionsJsonConverter(camelCase)); - _serializeSettings.Converters.Add(new StringEnumConverter(camelCase)); - } - - [Fact] - public void GivenNullJson_WhenReading_ThenDeserialize() - { - const string json = "null"; - - ExportDataOptions actual = JsonConvert.DeserializeObject>(json, _serializeSettings); - Assert.Null(actual); - } - - [Fact] - public void GivenInvalidSourceOptionsJson_WhenReading_ThenDeserialize() - { - const string json = @"{ - ""type"": ""identifiers"", - ""settings"": { - ""flag"": true, - ""hello"": [ - ""w"", - ""o"", - ""r"", - ""l"", - ""d"" - ] - } -}"; - - ExportDataOptions actual = JsonConvert.DeserializeObject>(json, _serializeSettings); - Assert.Equal(ExportSourceType.Identifiers, actual.Type); - - var options = actual.Settings as IdentifierExportOptions; - Assert.Null(options.Values); - } - - [Fact] - public void GivenInvalidDestinationOptionsJson_WhenReading_ThenDeserialize() - { - const string json = @"{ - ""type"": ""azureblob"", - ""settings"": { - ""connnectionStrong"":""BlobEndpoint=https://unit-test.blob.core.windows.net/;Foo=Bar"", - ""blobContainerNamee"": ""mycontainer"" - } -}"; - - ExportDataOptions actual = JsonConvert.DeserializeObject>(json, _serializeSettings); - Assert.Equal(ExportDestinationType.AzureBlob, actual.Type); - - var options = actual.Settings as AzureBlobExportOptions; - Assert.Null(options.ConnectionString); - Assert.Null(options.BlobContainerName); - Assert.Null(options.BlobContainerUri); - } - - [Fact] - public void GivenSourceOptionsJson_WhenReading_ThenDeserialize() - { - const string json = @"{ - ""type"": ""identifiers"", - ""settings"": { - ""values"": [ - ""1.2"", - ""3.4.5/67"", - ""8.9.10/1.112.13/1.4"" - ] - } -}"; - - ExportDataOptions actual = JsonConvert.DeserializeObject>(json, _serializeSettings); - Assert.Equal(ExportSourceType.Identifiers, actual.Type); - - var options = actual.Settings as IdentifierExportOptions; - Assert.Equal(3, options.Values.Count); - Assert.True(options.Values.SequenceEqual( - new DicomIdentifier[] - { - DicomIdentifier.ForStudy("1.2"), - DicomIdentifier.ForSeries("3.4.5", "67"), - DicomIdentifier.ForInstance("8.9.10", "1.112.13", "1.4"), - })); - } - - [Fact] - public void GivenDestinationOptionsJson_WhenReading_ThenDeserialize() - { - const string json = @"{ - ""type"": ""azureblob"", - ""settings"": { - ""connectionString"": ""BlobEndpoint=https://unit-test.blob.core.windows.net/;Foo=Bar"", - ""blobContainerName"": ""mycontainer"", - ""blobContainerUri"": ""https://unit-test.blob.core.windows.net/mycontainer"", - ""secret"": { - ""name"": ""foo"", - ""version"": ""1"" - }, - ""useManagedIdentity"": true - } -}"; - - ExportDataOptions actual = JsonConvert.DeserializeObject>(json, _serializeSettings); - Assert.Equal(ExportDestinationType.AzureBlob, actual.Type); - - var options = actual.Settings as AzureBlobExportOptions; - Assert.Equal("BlobEndpoint=https://unit-test.blob.core.windows.net/;Foo=Bar", options.ConnectionString); - Assert.Equal("mycontainer", options.BlobContainerName); - Assert.Equal(new Uri("https://unit-test.blob.core.windows.net/mycontainer"), options.BlobContainerUri); - Assert.Equal("foo", options.Secret.Name); - Assert.Equal("1", options.Secret.Version); - Assert.True(options.UseManagedIdentity); - } - - [Fact] - public void GivenNullValue_WhenWriting_ThenSerialize() - { - ExportDataOptions value = null; - string actual = JsonConvert.SerializeObject(value, _serializeSettings); - Assert.Equal("null", actual); - } - - [Fact] - public void GivenSourceOptions_WhenWriting_ThenSerialize() - { - var expected = new ExportDataOptions( - ExportSourceType.Identifiers, - new IdentifierExportOptions - { - Values = new DicomIdentifier[] - { - DicomIdentifier.ForStudy("1.2"), - DicomIdentifier.ForSeries("3.4.5", "67"), - DicomIdentifier.ForInstance("8.9.10", "1.112.13", "1.4"), - } - }); - - string actual = JsonConvert.SerializeObject(expected, _serializeSettings); - Assert.Equal( -@"{ - ""type"": ""identifiers"", - ""settings"": { - ""values"": [ - ""1.2"", - ""3.4.5/67"", - ""8.9.10/1.112.13/1.4"" - ] - } -}", - actual); - } - - [Fact] - public void GivenDestinationOptions_WhenWriting_ThenSerialize() - { - var expected = new ExportDataOptions( - ExportDestinationType.AzureBlob, - new AzureBlobExportOptions - { - ConnectionString = "BlobEndpoint=https://unit-test.blob.core.windows.net/;Foo=Bar", - BlobContainerName = "mycontainer", - BlobContainerUri = new Uri("https://unit-test.blob.core.windows.net/mycontainer"), - Secret = new SecretKey { Name = "foo", Version = "1" }, - UseManagedIdentity = true, - }); - - string actual = JsonConvert.SerializeObject(expected, _serializeSettings); - Assert.Equal( -@"{ - ""type"": ""azureBlob"", - ""settings"": { - ""blobContainerUri"": ""https://unit-test.blob.core.windows.net/mycontainer"", - ""connectionString"": ""BlobEndpoint=https://unit-test.blob.core.windows.net/;Foo=Bar"", - ""blobContainerName"": ""mycontainer"", - ""useManagedIdentity"": true, - ""secret"": { - ""name"": ""foo"", - ""version"": ""1"" - } - } -}", - actual); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Serialization/StrictStringEnumConverterTTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Serialization/StrictStringEnumConverterTTests.cs deleted file mode 100644 index f1399e4ede..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Serialization/StrictStringEnumConverterTTests.cs +++ /dev/null @@ -1,111 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.IO; -using System.Reflection; -using System.Text; -using System.Text.Json; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Serialization; -using Xunit; -using Xunit.Sdk; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Serialization; - -public class StrictStringEnumConverterTTests -{ - private static readonly JsonSerializerOptions DefaultOptions = new JsonSerializerOptions(); - - [Fact] - public void GivenStrictStringEnumConverter_WhenCheckingNullHandling_ThenReturnFalse() - { - Assert.False(new StrictStringEnumConverter().HandleNull); - } - - [Theory] - [InlineData("1")] - [InlineData("\"studys\"")] - [InlineData("\"innstance\"")] - [InlineData("42")] - public void GivenInvalidEnumName_WhenReadingJson_ThenThrowJsonReaderException(string json) - { - var jsonReader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json)); - - Assert.True(jsonReader.Read()); - try - { - new StrictStringEnumConverter().Read(ref jsonReader, typeof(QueryTagLevel), DefaultOptions); - throw ThrowsException.ForNoException(typeof(JsonException)); - } - catch (Exception e) - { - if (e.GetType() != typeof(JsonException)) - { - // TODO: Update with new method on next version of xUnit - https://github.com/xunit/xunit/issues/2741 - throw ThrowsException.ForNoException(typeof(JsonException)); - } - } - } - - [Theory] - [InlineData("INSTANCE", QueryTagLevel.Instance)] - [InlineData("series", QueryTagLevel.Series)] - [InlineData("StUdY", QueryTagLevel.Study)] - public void GivenStringToken_WhenReadingJson_ThenReturnEnum(string name, QueryTagLevel expected) - { - var jsonReader = new Utf8JsonReader(Encoding.UTF8.GetBytes("\"" + name + "\"")); - - Assert.True(jsonReader.Read()); - QueryTagLevel actual = new StrictStringEnumConverter().Read(ref jsonReader, typeof(QueryTagLevel), DefaultOptions); - Assert.Equal(expected, actual); - } - - [Theory] - [InlineData(QueryTagLevel.Instance, "\"Instance\"")] - [InlineData(QueryTagLevel.Series, "\"Series\"")] - [InlineData(QueryTagLevel.Study, "\"Study\"")] - public void GivenEnumValue_WhenWritingJson_ThenWriteName(QueryTagLevel value, string expected) - { - using var buffer = new MemoryStream(); - var jsonWriter = new Utf8JsonWriter(buffer); - - new StrictStringEnumConverter().Write(jsonWriter, value, DefaultOptions); - - jsonWriter.Flush(); - buffer.Seek(0, SeekOrigin.Begin); - - using var reader = new StreamReader(buffer, Encoding.UTF8); - Assert.Equal(expected, reader.ReadToEnd()); - } - - [Theory] - [InlineData(BindingFlags.CreateInstance, "\"createInstance\"")] - [InlineData(BindingFlags.DoNotWrapExceptions, "\"doNotWrapExceptions\"")] - [InlineData(BindingFlags.Instance, "\"instance\"")] - public void GivenNamingPolicy_WhenWritingJson_ThenWriteCamelCase(BindingFlags value, string expected) - { - using var buffer = new MemoryStream(); - var jsonWriter = new Utf8JsonWriter(buffer); - - new StrictStringEnumConverter(JsonNamingPolicy.CamelCase).Write(jsonWriter, value, DefaultOptions); - - jsonWriter.Flush(); - buffer.Seek(0, SeekOrigin.Begin); - - using var reader = new StreamReader(buffer, Encoding.UTF8); - Assert.Equal(expected, reader.ReadToEnd()); - } - - [Fact] - public void GivenUndefinedEnumValue_WhenWritingJson_ThenThrowJsonException() - { - using var buffer = new MemoryStream(); - var jsonWriter = new Utf8JsonWriter(buffer); - - Assert.Throws( - () => new StrictStringEnumConverter().Write(jsonWriter, (QueryTagLevel)12, DefaultOptions)); - } -} diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Serialization/StrictStringEnumConverterTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Serialization/StrictStringEnumConverterTests.cs deleted file mode 100644 index 2dfd232dff..0000000000 --- a/src/Microsoft.Health.Dicom.Core.UnitTests/Serialization/StrictStringEnumConverterTests.cs +++ /dev/null @@ -1,154 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.IO; -using System.Reflection; -using System.Text; -using System.Text.Json; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Serialization; -using Xunit; -using Xunit.Sdk; - -namespace Microsoft.Health.Dicom.Core.UnitTests.Serialization; - -public class StrictStringEnumConverterTests -{ - private readonly JsonSerializerOptions _defaultOptions; - - public StrictStringEnumConverterTests() - { - _defaultOptions = new JsonSerializerOptions(); - _defaultOptions.Converters.Add(new StrictStringEnumConverter()); - } - - [Fact] - public void GivenInvalidTypes_WhenCheckingCanConvert_ThenReturnFalse() - { - var factory = new StrictStringEnumConverter(); - Assert.False(factory.CanConvert(typeof(int))); - Assert.False(factory.CanConvert(typeof(string))); - Assert.False(factory.CanConvert(typeof(object))); - } - - [Fact] - public void GivenValidTypes_WhenCheckingCanConvert_ThenReturnTrue() - { - var factory = new StrictStringEnumConverter(); - Assert.True(factory.CanConvert(typeof(SeekOrigin))); - Assert.False(factory.CanConvert(typeof(BindingFlags?))); - } - - [Theory] - [InlineData("\"1\"", false)] - [InlineData("2", false)] - [InlineData("[ 1, 2, 3 ]", false)] - [InlineData("\"1\"", true)] - [InlineData("2", true)] - [InlineData("[ 1, 2, 3 ]", true)] - public void GivenInvalidJson_WhenReadingJson_ThenThrowJsonException(string jsonValue, bool nullable) - { - var jsonReader = new Utf8JsonReader(Encoding.UTF8.GetBytes($"{{ \"Level\": {jsonValue} }}")); - - try - { - if (nullable) - { - JsonSerializer.Deserialize(ref jsonReader, _defaultOptions); - } - else - { - JsonSerializer.Deserialize(ref jsonReader, _defaultOptions); - } - - throw ThrowsException.ForNoException(typeof(JsonException)); - } - catch (Exception e) - { - if (e.GetType() != typeof(JsonException)) - { - // TODO: Update with new method on next version of xUnit - https://github.com/xunit/xunit/issues/2741 - throw ThrowsException.ForNoException(typeof(JsonException)); - } - } - } - - [Theory] - [InlineData("INSTANCE", QueryTagLevel.Instance, false)] - [InlineData("series", QueryTagLevel.Series, false)] - [InlineData("StUdY", QueryTagLevel.Study, false)] - [InlineData("INSTANCE", QueryTagLevel.Instance, true)] - [InlineData("series", QueryTagLevel.Series, true)] - [InlineData("StUdY", QueryTagLevel.Study, true)] - public void GivenValidJson_WhenReadingJson_ThenReturnParsedValue(string name, QueryTagLevel expected, bool nullable) - { - var jsonReader = new Utf8JsonReader(Encoding.UTF8.GetBytes($"{{ \"Level\": \"{name}\" }}")); - - QueryTagLevel actual = nullable - ? JsonSerializer.Deserialize(ref jsonReader, _defaultOptions).Level.GetValueOrDefault() - : JsonSerializer.Deserialize(ref jsonReader, _defaultOptions).Level; - Assert.Equal(expected, actual); - } - - [Fact] - public void GivenNullValue_WhenReadingNullableJson_ThenReturnNull() - { - var jsonReader = new Utf8JsonReader(Encoding.UTF8.GetBytes($"{{ \"Level\": null }}")); - - NullableExample actual = JsonSerializer.Deserialize(ref jsonReader, _defaultOptions); - Assert.Null(actual.Level); - } - - [Theory] - [InlineData(QueryTagLevel.Instance, "\"Instance\"", false)] - [InlineData(QueryTagLevel.Series, "\"Series\"", false)] - [InlineData(QueryTagLevel.Study, "\"Study\"", false)] - [InlineData(QueryTagLevel.Instance, "\"Instance\"", true)] - [InlineData(QueryTagLevel.Series, "\"Series\"", true)] - [InlineData(QueryTagLevel.Study, "\"Study\"", true)] - public void GivenEnumValue_WhenWritingJson_ThenWriteName(QueryTagLevel value, string expected, bool nullable) - { - string actual = nullable - ? JsonSerializer.Serialize(new NullableExample { Level = value }, _defaultOptions) - : JsonSerializer.Serialize(new Example { Level = value }, _defaultOptions); - - Assert.Equal($"{{\"Level\":{expected}}}", actual); - } - - [Theory] - [InlineData(BindingFlags.CreateInstance, "\"createInstance\"")] - [InlineData(BindingFlags.DoNotWrapExceptions, "\"doNotWrapExceptions\"")] - [InlineData(BindingFlags.Instance, "\"instance\"")] - public void GivenNamingPolicy_WhenWritingJson_ThenWriteCamelCase(BindingFlags flags, string expected) - { - var camelCaseOptions = new JsonSerializerOptions(); - camelCaseOptions.Converters.Add(new StrictStringEnumConverter(JsonNamingPolicy.CamelCase)); - - Assert.Equal(expected, JsonSerializer.Serialize(flags, camelCaseOptions)); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public void GivenUndefinedEnumValue_WhenWritingJson_ThenThrowJsonException(bool nullable) - { - object obj = nullable - ? new NullableExample { Level = (QueryTagLevel)12 } - : new Example { Level = (QueryTagLevel)12 }; - - Assert.Throws(() => JsonSerializer.Serialize(obj, _defaultOptions)); - } - - private sealed class Example - { - public QueryTagLevel Level { get; set; } - } - - private sealed class NullableExample - { - public QueryTagLevel? Level { get; set; } - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Configs/AuthenticationConfiguration.cs b/src/Microsoft.Health.Dicom.Core/Configs/AuthenticationConfiguration.cs deleted file mode 100644 index de19daa230..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Configs/AuthenticationConfiguration.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; - -namespace Microsoft.Health.Dicom.Core.Configs; - -public class AuthenticationConfiguration -{ - public string Audience { get; set; } - - public IEnumerable Audiences { get; set; } - - public string Authority { get; set; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Configs/CacheConfiguration.cs b/src/Microsoft.Health.Dicom.Core/Configs/CacheConfiguration.cs deleted file mode 100644 index 285d57e097..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Configs/CacheConfiguration.cs +++ /dev/null @@ -1,13 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Configs; - -public class CacheConfiguration -{ - public int MaxCacheSize { get; set; } - - public int MaxCacheAbsoluteExpirationInMinutes { get; set; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Configs/ContentLengthBackFillConfiguration.cs b/src/Microsoft.Health.Dicom.Core/Configs/ContentLengthBackFillConfiguration.cs deleted file mode 100644 index 34642a2b27..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Configs/ContentLengthBackFillConfiguration.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - - -using System; - -namespace Microsoft.Health.Dicom.Core.Configs; - -public class ContentLengthBackFillConfiguration -{ - /// - /// Gets or sets the operation id - /// - public Guid OperationId { get; set; } = Guid.Parse("1d7f8475-dea6-4ffb-be39-9ee7f7b89810"); -} diff --git a/src/Microsoft.Health.Dicom.Core/Configs/DataPartitionConfiguration.cs b/src/Microsoft.Health.Dicom.Core/Configs/DataPartitionConfiguration.cs deleted file mode 100644 index b0bf3dd8f8..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Configs/DataPartitionConfiguration.cs +++ /dev/null @@ -1,18 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Configs; - -/// -/// Configuration for data partition feature. -/// -public class DataPartitionConfiguration : CacheConfiguration -{ - public DataPartitionConfiguration() - { - MaxCacheSize = 10000; - MaxCacheAbsoluteExpirationInMinutes = 5; - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Configs/DeletedInstanceCleanupConfiguration.cs b/src/Microsoft.Health.Dicom.Core/Configs/DeletedInstanceCleanupConfiguration.cs deleted file mode 100644 index 2995ecfe3b..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Configs/DeletedInstanceCleanupConfiguration.cs +++ /dev/null @@ -1,41 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; - -namespace Microsoft.Health.Dicom.Core.Configs; - -public class DeletedInstanceCleanupConfiguration -{ - /// - /// The amount of time to wait before the initial attempt to cleanup an instance. - /// Default: 3 days - /// - public TimeSpan DeleteDelay { get; set; } = TimeSpan.FromDays(3); - - /// - /// The maximum number of times to attempt to cleanup a deleted entry. - /// Default: 5 - /// - public int MaxRetries { get; set; } = 5; - - /// - /// The amount of time to back off between cleanup retries. - /// Default: 1 day - /// - public TimeSpan RetryBackOff { get; set; } = TimeSpan.FromDays(1); - - /// - /// The amount of time to wait between polling for new entries to cleanup. - /// Default: 3 minutes - /// - public TimeSpan PollingInterval { get; set; } = TimeSpan.FromMinutes(3); - - /// - /// The number of items to grab per batch. - /// Default: 10 - /// - public int BatchSize { get; set; } = 10; -} diff --git a/src/Microsoft.Health.Dicom.Core/Configs/ExtendedQueryTagConfiguration.cs b/src/Microsoft.Health.Dicom.Core/Configs/ExtendedQueryTagConfiguration.cs deleted file mode 100644 index fd5947b2de..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Configs/ExtendedQueryTagConfiguration.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Configs; - -/// -/// Configuration for extended query tag feature. -/// -public class ExtendedQueryTagConfiguration -{ - /// - /// Maximum allowed number of tags. - /// - public int MaxAllowedCount { get; set; } = 128; -} diff --git a/src/Microsoft.Health.Dicom.Core/Configs/FeatureConfiguration.cs b/src/Microsoft.Health.Dicom.Core/Configs/FeatureConfiguration.cs deleted file mode 100644 index 6ffcea11dc..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Configs/FeatureConfiguration.cs +++ /dev/null @@ -1,42 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Configs; - -public class FeatureConfiguration -{ - /// - /// Gets or sets a value indicating OHIF viewer should be enabled or not. - /// - public bool EnableOhifViewer { get; set; } - - /// - /// Enables stricter validation of each DicomItem value based on their VR type - /// - public bool EnableFullDicomItemValidation { get; set; } - - /// - /// Enables Data Partition feature. - /// - public bool EnableDataPartitions { get; set; } - - /// - /// Gets or sets a value indicating whether bulk export is enabled. - /// - public bool EnableExport { get; set; } - - /// - /// Enables the latest API version. - /// - /// Use as feature flag to test new API versions. - public bool EnableLatestApiVersion { get; set; } - - public bool EnableExternalStore { get; set; } - - /// - /// Disables all async operations - /// - public bool DisableOperations { get; set; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Configs/FramesRangeCacheConfiguration.cs b/src/Microsoft.Health.Dicom.Core/Configs/FramesRangeCacheConfiguration.cs deleted file mode 100644 index df91539c6b..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Configs/FramesRangeCacheConfiguration.cs +++ /dev/null @@ -1,15 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Configs; - -public class FramesRangeCacheConfiguration : CacheConfiguration -{ - public FramesRangeCacheConfiguration() - { - MaxCacheSize = 1000; - MaxCacheAbsoluteExpirationInMinutes = 5; - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Configs/HealthCheckPublisherConfiguration.cs b/src/Microsoft.Health.Dicom.Core/Configs/HealthCheckPublisherConfiguration.cs deleted file mode 100644 index a9b272085b..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Configs/HealthCheckPublisherConfiguration.cs +++ /dev/null @@ -1,25 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; - -namespace Microsoft.Health.Dicom.Core.Configs; - -public class HealthCheckPublisherConfiguration -{ - public const string SectionName = "HealthCheckPublisher"; - - /// - /// A comma separated list of health check names to exclude. - /// Example: "DcmHealthCheck,MetadataHealthCheck" - /// - public string ExcludedHealthCheckNames { get; set; } - - public IReadOnlyList GetListOfExcludedHealthCheckNames() - { - return ExcludedHealthCheckNames?.Split(',') ?? Array.Empty(); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Configs/InstanceDataCleanupConfiguration.cs b/src/Microsoft.Health.Dicom.Core/Configs/InstanceDataCleanupConfiguration.cs deleted file mode 100644 index 837a629738..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Configs/InstanceDataCleanupConfiguration.cs +++ /dev/null @@ -1,26 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; - -namespace Microsoft.Health.Dicom.Core.Configs; - -public class InstanceDataCleanupConfiguration -{ - /// - /// Gets or sets the operation id - /// - public Guid OperationId { get; set; } = Guid.Parse("f0a54b2a-eeca-4c45-af90-52ac15f6d486"); - - /// - /// Gets or sets the start time stamp for clean up - /// - public DateTimeOffset StartTimeStamp { get; set; } = new DateTimeOffset(new DateTime(2023, 06, 01, 0, 0, 0), TimeSpan.Zero); - - /// - /// Gets or sets the end time stamp for clean up - /// - public DateTimeOffset EndTimeStamp { get; set; } = new DateTimeOffset(new DateTime(2023, 09, 30, 0, 0, 0), TimeSpan.Zero); -} diff --git a/src/Microsoft.Health.Dicom.Core/Configs/InstanceMetadataCacheConfiguration.cs b/src/Microsoft.Health.Dicom.Core/Configs/InstanceMetadataCacheConfiguration.cs deleted file mode 100644 index 8f565f903f..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Configs/InstanceMetadataCacheConfiguration.cs +++ /dev/null @@ -1,15 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Configs; - -public class InstanceMetadataCacheConfiguration : CacheConfiguration -{ - public InstanceMetadataCacheConfiguration() - { - MaxCacheSize = 1000; - MaxCacheAbsoluteExpirationInMinutes = 5; - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Configs/RetrieveConfiguration.cs b/src/Microsoft.Health.Dicom.Core/Configs/RetrieveConfiguration.cs deleted file mode 100644 index 45aee0f220..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Configs/RetrieveConfiguration.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.ComponentModel.DataAnnotations; - -namespace Microsoft.Health.Dicom.Core.Configs; - -public class RetrieveConfiguration -{ - /// - /// Maximum dicom file supported for getting frames and transcoding - /// - public long MaxDicomFileSize { get; } = 1024 * 1024 * 100; //100 MB - - /// - /// Response uses lazy streams that copy data from storage JIT into a buffer and then to the output stream - /// This is the size of the buffer - /// - public int LazyResponseStreamBufferSize { get; } = 1024 * 1024 * 4; //4 MB - - /// - /// Gets or sets the maximum number of tasks that should be concurrently scheduled to read for a single metadata request. - /// - /// A positive number or -1 for unbounded parallelism. - [Range(-1, int.MaxValue)] - public int MaxDegreeOfParallelism { get; set; } = -1; -} diff --git a/src/Microsoft.Health.Dicom.Core/Configs/SecurityConfiguration.cs b/src/Microsoft.Health.Dicom.Core/Configs/SecurityConfiguration.cs deleted file mode 100644 index f1b8664b47..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Configs/SecurityConfiguration.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using Microsoft.Health.Core.Configs; -using Microsoft.Health.Dicom.Core.Features.Security; - -namespace Microsoft.Health.Dicom.Core.Configs; - -public class SecurityConfiguration -{ - public bool Enabled { get; set; } - - public AuthenticationConfiguration Authentication { get; set; } = new AuthenticationConfiguration(); - - public virtual HashSet PrincipalClaims { get; } = new HashSet(StringComparer.Ordinal); - - public AuthorizationConfiguration Authorization { get; set; } = new AuthorizationConfiguration(); -} diff --git a/src/Microsoft.Health.Dicom.Core/Configs/ServicesConfiguration.cs b/src/Microsoft.Health.Dicom.Core/Configs/ServicesConfiguration.cs deleted file mode 100644 index 0dde77c652..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Configs/ServicesConfiguration.cs +++ /dev/null @@ -1,29 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Configs; - -public class ServicesConfiguration -{ - public DeletedInstanceCleanupConfiguration DeletedInstanceCleanup { get; } = new DeletedInstanceCleanupConfiguration(); - - public StoreConfiguration StoreServiceSettings { get; } = new StoreConfiguration(); - - public ExtendedQueryTagConfiguration ExtendedQueryTag { get; } = new ExtendedQueryTagConfiguration(); - - public DataPartitionConfiguration DataPartition { get; } = new DataPartitionConfiguration(); - - public RetrieveConfiguration Retrieve { get; } = new RetrieveConfiguration(); - - public InstanceMetadataCacheConfiguration InstanceMetadataCacheConfiguration { get; } = new InstanceMetadataCacheConfiguration(); - - public FramesRangeCacheConfiguration FramesRangeCacheConfiguration { get; } = new FramesRangeCacheConfiguration(); - - public UpdateConfiguration UpdateServiceSettings { get; } = new UpdateConfiguration(); - - public InstanceDataCleanupConfiguration DataCleanupConfiguration { get; } = new InstanceDataCleanupConfiguration(); - - public ContentLengthBackFillConfiguration ContentLengthBackFillConfiguration { get; } = new ContentLengthBackFillConfiguration(); -} diff --git a/src/Microsoft.Health.Dicom.Core/Configs/StoreConfiguration.cs b/src/Microsoft.Health.Dicom.Core/Configs/StoreConfiguration.cs deleted file mode 100644 index 512eaa5ae0..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Configs/StoreConfiguration.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Configs; - -public class StoreConfiguration -{ - /// - /// Maximum allowed request length per dicom file - /// - public long MaxAllowedDicomFileSize { get; set; } = 2147483648; -} diff --git a/src/Microsoft.Health.Dicom.Core/Configs/UpdateConfiguration.cs b/src/Microsoft.Health.Dicom.Core/Configs/UpdateConfiguration.cs deleted file mode 100644 index 5f58f2b9c4..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Configs/UpdateConfiguration.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Configs; - -public class UpdateConfiguration -{ - /// - /// Per block max size. 4MB by default. - /// - public int StageBlockSizeInBytes { get; set; } = 1024 * 1024 * 4; - - /// - /// Max size of a large DICOM item. 1MB by default - /// - public int LargeDicomItemsizeInBytes { get; set; } = 1024 * 1024; -} diff --git a/src/Microsoft.Health.Dicom.Core/DicomCoreResource.Designer.cs b/src/Microsoft.Health.Dicom.Core/DicomCoreResource.Designer.cs deleted file mode 100644 index 807eb5cb21..0000000000 --- a/src/Microsoft.Health.Dicom.Core/DicomCoreResource.Designer.cs +++ /dev/null @@ -1,1509 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Microsoft.Health.Dicom.Core { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class DicomCoreResource { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal DicomCoreResource() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.Health.Dicom.Core.DicomCoreResource", typeof(DicomCoreResource).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to The Dicom Tag Property {0} must be specified and must not be null, empty or whitespace.. - /// - internal static string AddExtendedQueryTagEntryPropertyNotSpecified { - get { - return ResourceManager.GetString("AddExtendedQueryTagEntryPropertyNotSpecified", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The attribute with tag '{0}' must be empty.. - /// - internal static string AttributeMustBeEmpty { - get { - return ResourceManager.GetString("AttributeMustBeEmpty", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The attribute with tag '{0}' must not be empty.. - /// - internal static string AttributeMustNotBeEmpty { - get { - return ResourceManager.GetString("AttributeMustNotBeEmpty", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The attribute with tag '{0}' cannot be present.. - /// - internal static string AttributeNotAllowed { - get { - return ResourceManager.GetString("AttributeNotAllowed", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cancel Reason not provided. - /// - internal static string CancelWorkitemUnknownReasonCode { - get { - return ResourceManager.GetString("CancelWorkitemUnknownReasonCode", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The limit must be between 1 and {0}.. - /// - internal static string ChangeFeedLimitOutOfRange { - get { - return ResourceManager.GetString("ChangeFeedLimitOutOfRange", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The offset cannot be a negative value.. - /// - internal static string ChangeFeedOffsetCannotBeNegative { - get { - return ResourceManager.GetString("ChangeFeedOffsetCannotBeNegative", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The prefix used to identify custom audit headers cannot be empty.. - /// - internal static string CustomHeaderPrefixCannotBeEmpty { - get { - return ResourceManager.GetString("CustomHeaderPrefixCannotBeEmpty", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Data partition already exists.. - /// - internal static string DataPartitionAlreadyExists { - get { - return ResourceManager.GetString("DataPartitionAlreadyExists", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Data partitions feature cannot be disabled while existing data has already been partitioned.. - /// - internal static string DataPartitionFeatureCannotBeDisabled { - get { - return ResourceManager.GetString("DataPartitionFeatureCannotBeDisabled", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Specified PartitionName does not exist.. - /// - internal static string DataPartitionNotFound { - get { - return ResourceManager.GetString("DataPartitionNotFound", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Data partitions feature is disabled.. - /// - internal static string DataPartitionsFeatureDisabled { - get { - return ResourceManager.GetString("DataPartitionsFeatureDisabled", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to PartitionName value is missing in the route segment.. - /// - internal static string DataPartitionsMissingPartition { - get { - return ResourceManager.GetString("DataPartitionsMissingPartition", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Dataset does not match SOP Class.. - /// - internal static string DatasetDoesNotMatchSOPClass { - get { - return ResourceManager.GetString("DatasetDoesNotMatchSOPClass", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The data store operation failed.. - /// - internal static string DataStoreOperationFailed { - get { - return ResourceManager.GetString("DataStoreOperationFailed", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid query: specified DateTime value '{0}' for attribute {1} contains offset which is not supported.. - /// - internal static string DateTimeWithOffsetNotSupported { - get { - return ResourceManager.GetString("DateTimeWithOffsetNotSupported", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Asynchronous operations are temporarily disabled. Please try again later.. - /// - internal static string DicomAsyncOperationDisabled { - get { - return ResourceManager.GetString("DicomAsyncOperationDisabled", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Dicom element '{0}' failed validation for VR '{1}': {2}. - /// - internal static string DicomElementValidationFailed { - get { - return ResourceManager.GetString("DicomElementValidationFailed", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to An error was encountered when attempting to convert the dicom file into an image. - /// - internal static string DicomImageConversionFailed { - get { - return ResourceManager.GetString("DicomImageConversionFailed", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Dicom update feature is disabled.. - /// - internal static string DicomUpdateFeatureDisabled { - get { - return ResourceManager.GetString("DicomUpdateFeatureDisabled", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to StudyInstanceUids count exceeded maximum length '{0}'. - /// - internal static string DicomUpdateStudyInstanceUidsExceedMaxCount { - get { - return ResourceManager.GetString("DicomUpdateStudyInstanceUidsExceedMaxCount", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Updating the tag is not supported. - /// - internal static string DicomUpdateTagValidationFailed { - get { - return ResourceManager.GetString("DicomUpdateTagValidationFailed", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid query: cannot specify attribute 'SeriesInstanceUID' for the given resource.. - /// - internal static string DisallowedSeriesInstanceUIDAttribute { - get { - return ResourceManager.GetString("DisallowedSeriesInstanceUIDAttribute", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid query: cannot specify attribute 'StudyInstanceUID' for the given resource. . - /// - internal static string DisallowedStudyInstanceUIDAttribute { - get { - return ResourceManager.GetString("DisallowedStudyInstanceUIDAttribute", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid query: attribute '{0}' has been specified more than once using different ID formats. Each attribute is only allowed to be specified once.. - /// - internal static string DuplicateAttribute { - get { - return ResourceManager.GetString("DuplicateAttribute", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The extended query tag '{0}' has been specified before.. - /// - internal static string DuplicateExtendedQueryTag { - get { - return ResourceManager.GetString("DuplicateExtendedQueryTag", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to There are {0} roles with the name '{1}'. - /// - internal static string DuplicateRoleNames { - get { - return ResourceManager.GetString("DuplicateRoleNames", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Duplicate value '{0}' for tag '{1}' not supported in a sequence.. - /// - internal static string DuplicateTagValueNotSupported { - get { - return ResourceManager.GetString("DuplicateTagValueNotSupported", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Value cannot be parsed as a valid date.. - /// - internal static string ErrorMessageDateIsInvalid { - get { - return ResourceManager.GetString("ErrorMessageDateIsInvalid", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Value cannot be parsed as a valid DateTime.. - /// - internal static string ErrorMessageDateTimeIsInvalid { - get { - return ResourceManager.GetString("ErrorMessageDateTimeIsInvalid", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Value length exceeds maximum length of {0}.. - /// - internal static string ErrorMessageExceedMaxLength { - get { - return ResourceManager.GetString("ErrorMessageExceedMaxLength", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to DICOM{0}: {1} - {2}. - /// - internal static string ErrorMessageFormat { - get { - return ResourceManager.GetString("ErrorMessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Value cannot be parsed as a valid integer string.. - /// - internal static string ErrorMessageIntegerStringIsInvalid { - get { - return ResourceManager.GetString("ErrorMessageIntegerStringIsInvalid", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Value contains invalid character.. - /// - internal static string ErrorMessageInvalidCharacters { - get { - return ResourceManager.GetString("ErrorMessageInvalidCharacters", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Dicom element has multiple values. Indexing is only supported on single value element.. - /// - internal static string ErrorMessageMultiValues { - get { - return ResourceManager.GetString("ErrorMessageMultiValues", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Value contains more than 5 components.. - /// - internal static string ErrorMessagePersonNameExceedMaxComponents { - get { - return ResourceManager.GetString("ErrorMessagePersonNameExceedMaxComponents", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Value contains more than 3 groups.. - /// - internal static string ErrorMessagePersonNameExceedMaxGroups { - get { - return ResourceManager.GetString("ErrorMessagePersonNameExceedMaxGroups", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to One or more group of person name exceeds maxium length of 64.. - /// - internal static string ErrorMessagePersonNameGroupExceedMaxLength { - get { - return ResourceManager.GetString("ErrorMessagePersonNameGroupExceedMaxLength", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Value cannot be parsed as a valid Time.. - /// - internal static string ErrorMessageTimeIsInvalid { - get { - return ResourceManager.GetString("ErrorMessageTimeIsInvalid", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to DICOM Identifier is invalid. Value length should not exceed the maximum length of 64 characters. Value should contain characters in '0'-'9' and '.'. Each component must start with non-zero number.. - /// - internal static string ErrorMessageUidIsInvalid { - get { - return ResourceManager.GetString("ErrorMessageUidIsInvalid", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Value length is not {0}.. - /// - internal static string ErrorMessageUnexpectedLength { - get { - return ResourceManager.GetString("ErrorMessageUnexpectedLength", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The extended query tag '{0}' is expected to have VR '{1}' but has '{2}' in file.. - /// - internal static string ErrorMessageUnexpectedVR { - get { - return ResourceManager.GetString("ErrorMessageUnexpectedVR", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Error validating roles: - ///{0}. - /// - internal static string ErrorValidatingRoles { - get { - return ResourceManager.GetString("ErrorValidatingRoles", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to There is already an active {0} operation with ID '{1}'.. - /// - internal static string ExistingOperation { - get { - return ResourceManager.GetString("ExistingOperation", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Extended Query Tag feature is disabled.. - /// - internal static string ExtendedQueryTagFeatureDisabled { - get { - return ResourceManager.GetString("ExtendedQueryTagFeatureDisabled", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The specified extended query tag with tag path {0} cannot be found.. - /// - internal static string ExtendedQueryTagNotFound { - get { - return ResourceManager.GetString("ExtendedQueryTagNotFound", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to One or more extended query tags already exist.. - /// - internal static string ExtendedQueryTagsAlreadyExists { - get { - return ResourceManager.GetString("ExtendedQueryTagsAlreadyExists", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Extended query tags exceeds max allowed count '{0}'.. - /// - internal static string ExtendedQueryTagsExceedsMaxAllowedCount { - get { - return ResourceManager.GetString("ExtendedQueryTagsExceedsMaxAllowedCount", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to One or more extended query tags have been modified.. - /// - internal static string ExtendedQueryTagsOutOfDate { - get { - return ResourceManager.GetString("ExtendedQueryTagsOutOfDate", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Received the following error code from blob storage account: {0}. Can not perform operation '{1}' because the blob '{2}' with eTag '{3}' has been modified or deleted by another process. Use the https://go.microsoft.com/fwlink/?linkid=2251550 to troubleshoot the issue.. - /// - internal static string ExternalDataStoreBlobModified { - get { - return ResourceManager.GetString("ExternalDataStoreBlobModified", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Error occurred during an operation on the configured storage account. Use the https://go.microsoft.com/fwlink/?linkid=2251550 to troubleshoot the issue. No such host is known.. - /// - internal static string ExternalDataStoreHostIsUnknown { - get { - return ResourceManager.GetString("ExternalDataStoreHostIsUnknown", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Error occurred during an operation on the configured storage account. Use the https://go.microsoft.com/fwlink/?linkid=2251550 to troubleshoot the issue. Received the following error code: {0}. - /// - internal static string ExternalDataStoreOperationFailed { - get { - return ResourceManager.GetString("ExternalDataStoreOperationFailed", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Error occurred during an operation on the configured storage account. Use the https://go.microsoft.com/fwlink/?linkid=2251550 to troubleshoot the issue.. - /// - internal static string ExternalDataStoreOperationFailedUnknownIssue { - get { - return ResourceManager.GetString("ExternalDataStoreOperationFailedUnknownIssue", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Authorization failed.. - /// - internal static string Forbidden { - get { - return ResourceManager.GetString("Forbidden", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The specified frame cannot be found.. - /// - internal static string FrameNotFound { - get { - return ResourceManager.GetString("FrameNotFound", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Implicit VR is not allowed.. - /// - internal static string ImplicitVRNotAllowed { - get { - return ResourceManager.GetString("ImplicitVRNotAllowed", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid query: IncludeField has unknown attribute '{0}'.. - /// - internal static string IncludeFieldUnknownAttribute { - get { - return ResourceManager.GetString("IncludeFieldUnknownAttribute", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to One or more indexed Dicom tag(s) have multiple values, only first value is indexed.. - /// - internal static string IndexedDicomTagHasMultipleValues { - get { - return ResourceManager.GetString("IndexedDicomTagHasMultipleValues", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The DICOM instance already exists.. - /// - internal static string InstanceAlreadyExists { - get { - return ResourceManager.GetString("InstanceAlreadyExists", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The specified instance cannot be found.. - /// - internal static string InstanceNotFound { - get { - return ResourceManager.GetString("InstanceNotFound", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Array rank {0} is not supported.. - /// - internal static string InvalidArrayRank { - get { - return ResourceManager.GetString("InvalidArrayRank", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid query: specified date range '{0}' is invalid. - ///The first part date {1} should be lesser than or equal to the second part date {2}.. - /// - internal static string InvalidDateRangeValue { - get { - return ResourceManager.GetString("InvalidDateRangeValue", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid query: specified date range '{0}' is invalid. - ///The first part DateTime {1} should be lesser than or equal to the second part DateTime {2}.. - /// - internal static string InvalidDateTimeRangeValue { - get { - return ResourceManager.GetString("InvalidDateTimeRangeValue", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid query: specified DateTime value '{0}' is invalid for attribute '{1}'. DateTime should be valid and formatted as yyyyMMddHHmmss.FFFFFF where yyyy is mandatory.. - /// - internal static string InvalidDateTimeValue { - get { - return ResourceManager.GetString("InvalidDateTimeValue", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid query: specified Date value '{0}' is invalid for attribute '{1}'. Date should be valid and formatted as yyyyMMdd.. - /// - internal static string InvalidDateValue { - get { - return ResourceManager.GetString("InvalidDateValue", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The string '{0}' was not recognized as a valid DICOM identifier.. - /// - internal static string InvalidDicomIdentifier { - get { - return ResourceManager.GetString("InvalidDicomIdentifier", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The DICOM instance is invalid.. - /// - internal static string InvalidDicomInstance { - get { - return ResourceManager.GetString("InvalidDicomInstance", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Input Dicom Tag Level '{0}' is invalid. It must have value 'Study', 'Series' or 'Instance'.. - /// - internal static string InvalidDicomTagLevel { - get { - return ResourceManager.GetString("InvalidDicomTagLevel", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid query: specified value '{0}' extended query tag with path '{1}' is not a valid double value.. - /// - internal static string InvalidDoubleValue { - get { - return ResourceManager.GetString("InvalidDoubleValue", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The extended query tag '{0}' is invalid as it cannot be parsed into a valid Dicom Tag.. - /// - internal static string InvalidExtendedQueryTag { - get { - return ResourceManager.GetString("InvalidExtendedQueryTag", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The specified frames value is not valid. At least one frame must be present, and all requested frames must have value greater than 0.. - /// - internal static string InvalidFramesValue { - get { - return ResourceManager.GetString("InvalidFramesValue", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid query: specified fuzzymatch value '{0}' is not a valid boolean. - /// - internal static string InvalidFuzzyMatchValue { - get { - return ResourceManager.GetString("InvalidFuzzyMatchValue", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Image quality must be between 1 and 100 inclusive. - /// - internal static string InvalidImageQuality { - get { - return ResourceManager.GetString("InvalidImageQuality", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid query: cannot specify included fields in addition to 'all'.. - /// - internal static string InvalidIncludeAllFields { - get { - return ResourceManager.GetString("InvalidIncludeAllFields", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to MaxBufferedItems must be greater than MaxDegreeOfParallelism if not unbounded.. - /// - internal static string InvalidItemBuffering { - get { - return ResourceManager.GetString("InvalidItemBuffering", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Read invalid JSON token type '{0}'.. - /// - internal static string InvalidJsonToken { - get { - return ResourceManager.GetString("InvalidJsonToken", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid query: specified limit value '{0}' is not a valid integer.. - /// - internal static string InvalidLimitValue { - get { - return ResourceManager.GetString("InvalidLimitValue", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid query: specified value '{0}' extended query tag with path '{1}' is not a valid long value.. - /// - internal static string InvalidLongValue { - get { - return ResourceManager.GetString("InvalidLongValue", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid query: specified offset value '{0}' is not a valid integer.. - /// - internal static string InvalidOffsetValue { - get { - return ResourceManager.GetString("InvalidOffsetValue", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The specified PartitionName is invalid. . - /// - internal static string InvalidPartitionName { - get { - return ResourceManager.GetString("InvalidPartitionName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The workitem instance can not transition to '{0}' state: {1}.. - /// - internal static string InvalidProcedureStepStateTransition { - get { - return ResourceManager.GetString("InvalidProcedureStepStateTransition", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The query string included invalid characters.. - /// - internal static string InvalidQueryString { - get { - return ResourceManager.GetString("InvalidQueryString", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The query parameter '{0}' is invalid. {1}. - /// - internal static string InvalidQueryStringValue { - get { - return ResourceManager.GetString("InvalidQueryStringValue", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid query: specified range is invalid. - ///Both parts in the range cannot be empty. - ///For details on valid range queries, please refer to Search Matching section in Conformance Statement (https://github.com/microsoft/dicom-server/blob/main/docs/resources/conformance-statement.md#search-matching).. - /// - internal static string InvalidRangeValues { - get { - return ResourceManager.GetString("InvalidRangeValues", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid query: specified tag '{0}' value '{1}' contains unsupported character '{2}'. Character is not supported with Fuzzy match, try exact match or removing the invalid character. . - /// - internal static string InvalidTagValueWithFuzzyMatch { - get { - return ResourceManager.GetString("InvalidTagValueWithFuzzyMatch", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid query: specified date range '{0}' is invalid. - ///The first part time {1} should be lesser than or equal to the second part time {2}.. - /// - internal static string InvalidTimeRangeValue { - get { - return ResourceManager.GetString("InvalidTimeRangeValue", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid query: specified Time value '{0}' is invalid for parameter '{1}'. Time should be valid and formatted as HHmmss.FFFFFF where HH is mandatory.. - /// - internal static string InvalidTimeValue { - get { - return ResourceManager.GetString("InvalidTimeValue", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid TransactionUID for the specified Workitem Instance UID.. - /// - internal static string InvalidTransactionUID { - get { - return ResourceManager.GetString("InvalidTransactionUID", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The specified Transfer Syntax value is not valid.. - /// - internal static string InvalidTransferSyntaxValue { - get { - return ResourceManager.GetString("InvalidTransferSyntaxValue", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Type '{0}' is not supported.. - /// - internal static string InvalidType { - get { - return ResourceManager.GetString("InvalidType", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The VR code '{0}' for tag '{1}' is invalid.. - /// - internal static string InvalidVRCode { - get { - return ResourceManager.GetString("InvalidVRCode", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The target URI did not reference a claimed Workitem.. - /// - internal static string InvalidWorkitemInstanceTargetUri { - get { - return ResourceManager.GetString("InvalidWorkitemInstanceTargetUri", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The specified item cannot be found.. - /// - internal static string ItemNotFound { - get { - return ResourceManager.GetString("ItemNotFound", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to SOPInstanceUID in the payload does not match the workitem query parameter.. - /// - internal static string MismatchSopInstanceWorkitemInstanceUid { - get { - return ResourceManager.GetString("MismatchSopInstanceWorkitemInstanceUid", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The StudyInstanceUid in the payload does not match the specified StudyInstanceUid.. - /// - internal static string MismatchStudyInstanceUid { - get { - return ResourceManager.GetString("MismatchStudyInstanceUid", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The extended query tag(s) is missing.. - /// - internal static string MissingExtendedQueryTag { - get { - return ResourceManager.GetString("MissingExtendedQueryTag", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The level for extended query tag '{0}' is missing.. - /// - internal static string MissingLevel { - get { - return ResourceManager.GetString("MissingLevel", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The private creator for private tag '{0}' is missing.. - /// - internal static string MissingPrivateCreator { - get { - return ResourceManager.GetString("MissingPrivateCreator", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Missing value for property '{0}'.. - /// - internal static string MissingProperty { - get { - return ResourceManager.GetString("MissingProperty", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The request body is missing.. - /// - internal static string MissingRequestBody { - get { - return ResourceManager.GetString("MissingRequestBody", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The required field '{0}' is missing or empty.. - /// - internal static string MissingRequiredField { - get { - return ResourceManager.GetString("MissingRequiredField", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The required tag '{0}' is missing.. - /// - internal static string MissingRequiredTag { - get { - return ResourceManager.GetString("MissingRequiredTag", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The value for tag '{0}' is missing.. - /// - internal static string MissingRequiredValue { - get { - return ResourceManager.GetString("MissingRequiredValue", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The vr for tag '{0}' is missing.. - /// - internal static string MissingVRCode { - get { - return ResourceManager.GetString("MissingVRCode", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The request contains multiple accept headers, which is not supported.. - /// - internal static string MultipleAcceptHeadersNotSupported { - get { - return ResourceManager.GetString("MultipleAcceptHeadersNotSupported", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Nested Dicom sequence tags are currently not supported.. - /// - internal static string NestedSequencesNotSupported { - get { - return ResourceManager.GetString("NestedSequencesNotSupported", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The request headers are not acceptable. - /// - internal static string NotAcceptableHeaders { - get { - return ResourceManager.GetString("NotAcceptableHeaders", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Specified limit value {0} is outside the allowed range of {1}..{2}.. - /// - internal static string PaginationLimitOutOfRange { - get { - return ResourceManager.GetString("PaginationLimitOutOfRange", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Specified offset value {0} cannot be negative.. - /// - internal static string PaginationNegativeOffset { - get { - return ResourceManager.GetString("PaginationNegativeOffset", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The private creator is not empty for standard tag '{0}'.. - /// - internal static string PrivateCreatorNotEmpty { - get { - return ResourceManager.GetString("PrivateCreatorNotEmpty", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The private creator is not empty for private identification code '{0}'.. - /// - internal static string PrivateCreatorNotEmptyForPrivateIdentificationCode { - get { - return ResourceManager.GetString("PrivateCreatorNotEmptyForPrivateIdentificationCode", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The private creator for tag '{0}' is not a valid LO attribute.. - /// - internal static string PrivateCreatorNotValidLO { - get { - return ResourceManager.GetString("PrivateCreatorNotValidLO", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid query: AttributeId '{0}' has empty string value that is not supported.. - /// - internal static string QueryEmptyAttributeValue { - get { - return ResourceManager.GetString("QueryEmptyAttributeValue", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid query: querying is only supported at resource level Studies/Series/Instances.. - /// - internal static string QueryInvalidResourceLevel { - get { - return ResourceManager.GetString("QueryInvalidResourceLevel", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Query is disabled on specified attribute '{0}'.. - /// - internal static string QueryIsDisabledOnAttribute { - get { - return ResourceManager.GetString("QueryIsDisabledOnAttribute", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid query: specified limit value {0} is outside the allowed range of {1}..{2}.. - /// - internal static string QueryResultCountMaxExceeded { - get { - return ResourceManager.GetString("QueryResultCountMaxExceeded", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The query tag '{0}' is already supported.. - /// - internal static string QueryTagAlreadySupported { - get { - return ResourceManager.GetString("QueryTagAlreadySupported", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Requested DICOM instance size is above the supported limit of {0} for rendering. Please retrieve the whole file in the original transfer syntax instead of rendering to image media type.. - /// - internal static string RenderFileTooLarge { - get { - return ResourceManager.GetString("RenderFileTooLarge", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The request exceeded the allowed limit of {0} bytes.. - /// - internal static string RequestLengthLimitExceeded { - get { - return ResourceManager.GetString("RequestLengthLimitExceeded", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The required condition for tag '{0}' is not met.. - /// - internal static string RequiredConditionNotMet { - get { - return ResourceManager.GetString("RequiredConditionNotMet", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Requested DICOM instance size is above the supported limit of {0}. The entire instance can be retrieved with the original transfer syntax by specifying 'transfer-syntax=*' in the Accept header.. - /// - internal static string RetrieveServiceFileTooBig { - get { - return ResourceManager.GetString("RetrieveServiceFileTooBig", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Accept transfer syntax '{0}' is not supported when multiple instances are matched. Matching instances can be retrieved with their original transfer syntax by specifying 'transfer-syntax=*' in the Accept header.. - /// - internal static string RetrieveServiceMultiInstanceTranscodingNotSupported { - get { - return ResourceManager.GetString("RetrieveServiceMultiInstanceTranscodingNotSupported", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Sequential dicom tags are currently not supported.. - /// - internal static string SequentialDicomTagsNotSupported { - get { - return ResourceManager.GetString("SequentialDicomTagsNotSupported", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The specified series cannot be found.. - /// - internal static string SeriesInstanceNotFound { - get { - return ResourceManager.GetString("SeriesInstanceNotFound", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The specified series cannot be found.. - /// - internal static string SeriesNotFound { - get { - return ResourceManager.GetString("SeriesNotFound", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The server is currently unable to receive requests. Please retry your request. If the issue persists, please contact support.. - /// - internal static string ServiceUnavailable { - get { - return ResourceManager.GetString("ServiceUnavailable", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Value length exceeds maximum length.. - /// - internal static string SimpleErrorMessageExceedMaxLength { - get { - return ResourceManager.GetString("SimpleErrorMessageExceedMaxLength", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Value length is not expected.. - /// - internal static string SimpleErrorMessageUnexpectedLength { - get { - return ResourceManager.GetString("SimpleErrorMessageUnexpectedLength", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The extended query tag VR is not expected.. - /// - internal static string SimpleErrorMessageUnexpectedVR { - get { - return ResourceManager.GetString("SimpleErrorMessageUnexpectedVR", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The accept header is supported for single frame retrieval, use multi-part accept header instead.. - /// - internal static string SinglePartSupportedForSingleFrame { - get { - return ResourceManager.GetString("SinglePartSupportedForSingleFrame", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The specified study cannot be found.. - /// - internal static string StudyInstanceNotFound { - get { - return ResourceManager.GetString("StudyInstanceNotFound", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The specified study cannot be found.. - /// - internal static string StudyNotFound { - get { - return ResourceManager.GetString("StudyNotFound", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The transaction UID is missing.. - /// - internal static string TransactionUIDAbsent { - get { - return ResourceManager.GetString("TransactionUIDAbsent", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Expected token type '{0}' but read '{1}' instead.. - /// - internal static string UnexpectedJsonToken { - get { - return ResourceManager.GetString("UnexpectedJsonToken", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to '{0}' DicomTag is not expected for '{1}' operation.. - /// - internal static string UnexpectedTag { - get { - return ResourceManager.GetString("UnexpectedTag", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Expected value '{0}' to be one of the following values: [{1}]. - /// - internal static string UnexpectedValue { - get { - return ResourceManager.GetString("UnexpectedValue", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid query: unknown query parameter '{0}'. If the parameter is an attribute keyword, check the casing as they are case-sensitive. The conformance statement has a list of supported query parameters, attributes and the levels.. - /// - internal static string UnknownQueryParameter { - get { - return ResourceManager.GetString("UnknownQueryParameter", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cannot buffer items if parallelism is unbounded.. - /// - internal static string UnsupportedBuffering { - get { - return ResourceManager.GetString("UnsupportedBuffering", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The specified content type '{0}' is not supported.. - /// - internal static string UnsupportedContentType { - get { - return ResourceManager.GetString("UnsupportedContentType", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Unsupported export destination '{0}'.. - /// - internal static string UnsupportedExportDestination { - get { - return ResourceManager.GetString("UnsupportedExportDestination", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Unsupported export source '{0}'.. - /// - internal static string UnsupportedExportSource { - get { - return ResourceManager.GetString("UnsupportedExportSource", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid query: AttributeId {0} is not queryable. If the parameter is an attribute keyword, check the casing as they are case-sensitive. The conformance statement has a list of supported query parameters, attributes and the levels.. - /// - internal static string UnsupportedSearchParameter { - get { - return ResourceManager.GetString("UnsupportedSearchParameter", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The specified transcoding is not supported.. - /// - internal static string UnsupportedTranscoding { - get { - return ResourceManager.GetString("UnsupportedTranscoding", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The VR code '{0}' specified for tag '{1}' is not supported.. - /// - internal static string UnsupportedVRCode { - get { - return ResourceManager.GetString("UnsupportedVRCode", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The VR code '{0}' is incorrectly specified for '{1}'. The expected VR code for it is '{2}'. Retry this request either with the correct VR code or without specifying it.. - /// - internal static string UnsupportedVRCodeOnTag { - get { - return ResourceManager.GetString("UnsupportedVRCodeOnTag", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid workitem query. AttributeId {0} is not queryable. If the parameter is an attribute keyword, check the casing as they are case-sensitive. The conformance statement has a list of supported query parameters and attributes.. - /// - internal static string UnsupportedWorkitemSearchParameter { - get { - return ResourceManager.GetString("UnsupportedWorkitemSearchParameter", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The submitted request is inconsistent with the current state of the Workitem.. - /// - internal static string UpdateWorkitemInstanceConflictFailure { - get { - return ResourceManager.GetString("UpdateWorkitemInstanceConflictFailure", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to This operation does not support value types.. - /// - internal static string ValueTypesNotSupported { - get { - return ResourceManager.GetString("ValueTypesNotSupported", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The UPS may no longer be updated.. - /// - internal static string WorkitemCancelRequestRejected { - get { - return ResourceManager.GetString("WorkitemCancelRequestRejected", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The workitem instance has been cancelled.. - /// - internal static string WorkitemCancelRequestSuccess { - get { - return ResourceManager.GetString("WorkitemCancelRequestSuccess", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The workitem instance is currently being updated. Please try again later.. - /// - internal static string WorkitemCurrentlyBeingUpdated { - get { - return ResourceManager.GetString("WorkitemCurrentlyBeingUpdated", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The workitem instance already exists. Try creating using a different UID.. - /// - internal static string WorkitemInstanceAlreadyExists { - get { - return ResourceManager.GetString("WorkitemInstanceAlreadyExists", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The workitem instance is not found.. - /// - internal static string WorkitemInstanceNotFound { - get { - return ResourceManager.GetString("WorkitemInstanceNotFound", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The UPS is already in the requested state of CANCELED.. - /// - internal static string WorkitemIsAlreadyCanceled { - get { - return ResourceManager.GetString("WorkitemIsAlreadyCanceled", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The UPS is already COMPLETED.. - /// - internal static string WorkitemIsAlreadyCompleted { - get { - return ResourceManager.GetString("WorkitemIsAlreadyCompleted", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The workitem instance is already in the final state '{0}'. {1}.. - /// - internal static string WorkitemIsInFinalState { - get { - return ResourceManager.GetString("WorkitemIsInFinalState", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Procedure step state is present in the dataset provided to be updated which is not allowed.. - /// - internal static string WorkitemProcedureStepStateNotAllowed { - get { - return ResourceManager.GetString("WorkitemProcedureStepStateNotAllowed", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The workitem instance state is '{0}'.. - /// - internal static string WorkitemUpdateIsNotAllowed { - get { - return ResourceManager.GetString("WorkitemUpdateIsNotAllowed", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The workitem instance has been updated successfully.. - /// - internal static string WorkitemUpdateRequestSuccess { - get { - return ResourceManager.GetString("WorkitemUpdateRequestSuccess", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Following tags could not be updated as they were not present when the workitem was created: {0}. - /// - internal static string WorkitemUpdateWarningTags { - get { - return ResourceManager.GetString("WorkitemUpdateWarningTags", resourceCulture); - } - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core/DicomCoreResource.resx b/src/Microsoft.Health.Dicom.Core/DicomCoreResource.resx deleted file mode 100644 index c891ea5e03..0000000000 --- a/src/Microsoft.Health.Dicom.Core/DicomCoreResource.resx +++ /dev/null @@ -1,678 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - The limit must be between 1 and {0}. - {0} number that is the maximum - - - The offset cannot be a negative value. - - - The prefix used to identify custom audit headers cannot be empty. - - - The data store operation failed. - - - Dicom element '{0}' failed validation for VR '{1}': {2} - Dicom element {0} of VR {1}. {2} has more detailed message on why. - - - Invalid query: attribute '{0}' has been specified more than once using different ID formats. Each attribute is only allowed to be specified once. - {0} attribute-id - - - The specified frame cannot be found. - - - Invalid query: IncludeField has unknown attribute '{0}'. - {0} attribute name - - - The DICOM instance already exists. - - - The specified instance cannot be found. - - - The specified series cannot be found. - - - The specified study cannot be found. - - - Invalid query: specified date range '{0}' is invalid. -The first part date {1} should be lesser than or equal to the second part date {2}. - {0} date range with <date1>-<date2> format. {1} is date1. {2} is date2. - - - Invalid query: specified Date value '{0}' is invalid for attribute '{1}'. Date should be valid and formatted as yyyyMMdd. - {0} Date value. {1} attribute name. - - - DICOM Identifier is invalid. Value length should not exceed the maximum length of 64 characters. Value should contain characters in '0'-'9' and '.'. Each component must start with non-zero number. - - - The DICOM instance is invalid. - - - The specified frames value is not valid. At least one frame must be present, and all requested frames must have value greater than 0. - - - Invalid query: specified fuzzymatch value '{0}' is not a valid boolean - - - Invalid query: specified limit value '{0}' is not a valid integer. - - - Invalid query: specified offset value '{0}' is not a valid integer. - - - The query string included invalid characters. - - - The query parameter '{0}' is invalid. {1} - {0} is a parameter name. {1} is the error. - - - The specified Transfer Syntax value is not valid. - - - The specified item cannot be found. - - - The StudyInstanceUid in the payload does not match the specified StudyInstanceUid. - StudyInstanceUID is defined by the spec and does not need to be translated. - - - The request body is missing. - - - The required tag '{0}' is missing. - {0} is the DICOM tag name. E.g., StudyInstanceUID. - - - The request headers are not acceptable - - - Invalid query: AttributeId '{0}' has empty string value that is not supported. - {0} AttributeId name - - - Invalid query: querying is only supported at resource level Studies/Series/Instances. - - - Invalid query: specified limit value {0} is outside the allowed range of {1}..{2}. - {0} Specified query result limit. {1} min allowed. {2} max allowed. - - - The specified series cannot be found. - - - The server is currently unable to receive requests. Please retry your request. If the issue persists, please contact support. - - - The specified study cannot be found. - - - Invalid query: unknown query parameter '{0}'. If the parameter is an attribute keyword, check the casing as they are case-sensitive. The conformance statement has a list of supported query parameters, attributes and the levels. - {0} Parameter name - - - The specified content type '{0}' is not supported. - {0} is the specified content type. E.g., application/dicom - - - Invalid query: AttributeId {0} is not queryable. If the parameter is an attribute keyword, check the casing as they are case-sensitive. The conformance statement has a list of supported query parameters, attributes and the levels. - {0} AttributeId name. - - - The specified transcoding is not supported. - - - Value contains invalid character. - - - Value contains more than 5 components. - - - Value contains more than 3 groups. - - - Value cannot be parsed as a valid date. - - - The request contains multiple accept headers, which is not supported. - - - The extended query tag '{0}' has been specified before. - {0} Dicom Tag. - - - The extended query tag '{0}' is invalid as it cannot be parsed into a valid Dicom Tag. - {0} Dicom Tag - - - The VR code '{0}' for tag '{1}' is invalid. - {0} VR code. {1} Dicom Tag. - - - The extended query tag(s) is missing. - - - The vr for tag '{0}' is missing. - {0} is the DICOM tag path. - - - The VR code '{0}' is incorrectly specified for '{1}'. The expected VR code for it is '{2}'. Retry this request either with the correct VR code or without specifying it. - {0} Specified VR Code. {1} Dicom Tag. {2} Correct VR Code for Tag. - - - The VR code '{0}' specified for tag '{1}' is not supported. - {0} VR Code. {1} Dicom Tag. - - - Authorization failed. - - - Error validating roles: -{0} - - - There are {0} roles with the name '{1}' - - - One or more extended query tags already exist. - - - The specified extended query tag with tag path {0} cannot be found. - {0} tagPath on request - - - Sequential dicom tags are currently not supported. - - - Invalid query: specified value '{0}' extended query tag with path '{1}' is not a valid double value. - {0} double value. {1} extended query tag path. - - - Invalid query: specified value '{0}' extended query tag with path '{1}' is not a valid long value. - {0} long value. {1} extended query tag path. - - - Value length exceeds maximum length of {0}. - {0} max length. - - - Value length is not {0}. - {0} required length. - - - The private creator is not empty for standard tag '{0}'. - {0} Dicom Tag - - - The private creator for private tag '{0}' is missing. - {0} is the DICOM tag path. - - - The private creator for tag '{0}' is not a valid LO attribute. - {0} Dicom Tag. - - - The private creator is not empty for private identification code '{0}'. - {0} Dicom Tag. - - - Extended Query Tag feature is disabled. - - - The query tag '{0}' is already supported. - {0} Dicom Tag. - - - The level for extended query tag '{0}' is missing. - {0} Dicom Tag. - - - The Dicom Tag Property {0} must be specified and must not be null, empty or whitespace. - {0} is the property that was not specified correctly. - - - Input Dicom Tag Level '{0}' is invalid. It must have value 'Study', 'Series' or 'Instance'. - {0} input Dicom Tag Level. - - - The request exceeded the allowed limit of {0} bytes. - {0} Max number of bytes allowed. - - - Extended query tags exceeds max allowed count '{0}'. - {0} max allowed count. - - - The extended query tag '{0}' is expected to have VR '{1}' but has '{2}' in file. - {0} dicom tag. {1} expected vr code. {2} vr in file. - - - One or more extended query tags have been modified. - - - Dicom element has multiple values. Indexing is only supported on single value element. - - - One or more group of person name exceeds maxium length of 64. - - - Value length exceeds maximum length. - - - Value length is not expected. - - - The extended query tag VR is not expected. - - - Specified limit value {0} is outside the allowed range of {1}..{2}. - {0} Specified page result limit. {1} min allowed. {2} max allowed. - - - Specified offset value {0} cannot be negative. - {0} Specified page offset. - - - Invalid query: cannot specify attribute 'SeriesInstanceUID' for the given resource. - - - Invalid query: cannot specify attribute 'StudyInstanceUID' for the given resource. - - - Invalid query: cannot specify included fields in addition to 'all'. - - - Query is disabled on specified attribute '{0}'. - '{0}' attribute in QIDO query. - - - Invalid query: specified date range '{0}' is invalid. -The first part DateTime {1} should be lesser than or equal to the second part DateTime {2}. - {0} DateTime range with <DateTime1>-<DateTime2> format. {1} is DateTime1. {2} is DateTime2. - - - Invalid query: specified DateTime value '{0}' is invalid for attribute '{1}'. DateTime should be valid and formatted as yyyyMMddHHmmss.FFFFFF where yyyy is mandatory. - {0} DateTime value. {1} attribute name. - - - Invalid query: specified date range '{0}' is invalid. -The first part time {1} should be lesser than or equal to the second part time {2}. - {0} Time range with <Time1>-<Time2> format. {1} is Time1. {2} is Time2. - - - Invalid query: specified Time value '{0}' is invalid for parameter '{1}'. Time should be valid and formatted as HHmmss.FFFFFF where HH is mandatory. - {0} Time value. {1} parameter name. - - - Value cannot be parsed as a valid DateTime. - - - Value cannot be parsed as a valid Time. - - - Invalid query: specified DateTime value '{0}' for attribute {1} contains offset which is not supported. - {0} DateTime value. {1} attribute name. - - - Invalid query: specified range is invalid. -Both parts in the range cannot be empty. -For details on valid range queries, please refer to Search Matching section in Conformance Statement (https://github.com/microsoft/dicom-server/blob/main/docs/resources/conformance-statement.md#search-matching). - - - The specified PartitionName is invalid. - - - Data partitions feature is disabled. - - - PartitionName value is missing in the route segment. - - - Specified PartitionName does not exist. - - - Data partitions feature cannot be disabled while existing data has already been partitioned. - - - Dicom update feature is disabled. - - - Implicit VR is not allowed. - - - Expected token type '{0}' but read '{1}' instead. - {0} Expected token type. {1} Actual token type. - - - Expected value '{0}' to be one of the following values: [{1}] - {0} Actual value. {1} Comma-separated list of expected values. - - - The workitem instance already exists. Try creating using a different UID. - - - Nested Dicom sequence tags are currently not supported. - - - The workitem instance can not transition to '{0}' state: {1}. - {0} ProcedureStepState. {1} Error code - - - SOPInstanceUID in the payload does not match the workitem query parameter. - - - Duplicate value '{0}' for tag '{1}' not supported in a sequence. - {0} String Value. {1} DicomTag. - - - '{0}' DicomTag is not expected for '{1}' operation. - {0} Dicom Tag. {1} Action/Operation. - - - Invalid TransactionUID for the specified Workitem Instance UID. - - - The workitem instance is not found. - - - The workitem instance is already in the final state '{0}'. {1}. - {0} Procedure Step State. {1} Error or Warning Code. - - - The workitem instance state is '{0}'. - {0} Current Procedure Step State - - - The workitem instance has been cancelled. - - - Invalid workitem query. AttributeId {0} is not queryable. If the parameter is an attribute keyword, check the casing as they are case-sensitive. The conformance statement has a list of supported query parameters and attributes. - {0} AttributeId name. - - - The attribute with tag '{0}' must be empty. - {0} Dicom Tag - - - The attribute with tag '{0}' cannot be present. - {0} Dicom Tag - - - The value for tag '{0}' is missing. - {0} is the DICOM tag name. E.g., StudyInstanceUID. - - - The required condition for tag '{0}' is not met. - {0} is the DICOM tag name. - - - Requested DICOM instance size is above the supported limit of {0}. The entire instance can be retrieved with the original transfer syntax by specifying 'transfer-syntax=*' in the Accept header. - {0} bytes representing the support max file size - - - Accept transfer syntax '{0}' is not supported when multiple instances are matched. Matching instances can be retrieved with their original transfer syntax by specifying 'transfer-syntax=*' in the Accept header. - {0} Accept header transfer syntax - - - Cancel Reason not provided - - - The UPS may no longer be updated. - - - The UPS is already in the requested state of CANCELED. - - - The UPS is already COMPLETED. - - - Value cannot be parsed as a valid integer string. - - - The string '{0}' was not recognized as a valid DICOM identifier. - {0} DICOM identifier - - - Read invalid JSON token type '{0}'. - {0} Actual token type. - - - Unsupported export destination '{0}'. - {0} Destination - - - Unsupported export source '{0}'. - {0} Source - - - One or more indexed Dicom tag(s) have multiple values, only first value is indexed. - - - Missing value for property '{0}'. - {0} Property Name - - - Array rank {0} is not supported. - {0} Array rank - - - Type '{0}' is not supported. - {0} Type name - - - This operation does not support value types. - - - The workitem instance is currently being updated. Please try again later. - - - The transaction UID is missing. - - - Procedure step state is present in the dataset provided to be updated which is not allowed. - - - The workitem instance has been updated successfully. - - - The attribute with tag '{0}' must not be empty. - {0} Dicom Tag. - - - Following tags could not be updated as they were not present when the workitem was created: {0} - {0} comma separated list of tags that could not be updated. - - - The target URI did not reference a claimed Workitem. - - - There is already an active {0} operation with ID '{1}'. - {0} Operation type. {1} Operation ID - - - The submitted request is inconsistent with the current state of the Workitem. - - - The accept header is supported for single frame retrieval, use multi-part accept header instead. - - - MaxBufferedItems must be greater than MaxDegreeOfParallelism if not unbounded. - - - Cannot buffer items if parallelism is unbounded. - - - Invalid query: specified tag '{0}' value '{1}' contains unsupported character '{2}'. Character is not supported with Fuzzy match, try exact match or removing the invalid character. - {0} Dicom Tag name, {1} Tag value to search. {2} Unsupported character. - - - Dataset does not match SOP Class. - - - Data partition already exists. - - - DICOM{0}: {1} - {2} - - - An error was encountered when attempting to convert the dicom file into an image - - - Requested DICOM instance size is above the supported limit of {0} for rendering. Please retrieve the whole file in the original transfer syntax instead of rendering to image media type. - {0} bytes representing the support max file size - - - Updating the tag is not supported - - - The required field '{0}' is missing or empty. - {0} is the required field in input. E.g., StudyInstanceUIDs. - - - StudyInstanceUids count exceeded maximum length '{0}' - {0} StudyInstanceUids max allowed count - - - Image quality must be between 1 and 100 inclusive - - - Error occurred during an operation on the configured storage account. Use the https://go.microsoft.com/fwlink/?linkid=2251550 to troubleshoot the issue. Received the following error code: {0} - - - Error occurred during an operation on the configured storage account. Use the https://go.microsoft.com/fwlink/?linkid=2251550 to troubleshoot the issue. No such host is known. - - - Error occurred during an operation on the configured storage account. Use the https://go.microsoft.com/fwlink/?linkid=2251550 to troubleshoot the issue. - - - Received the following error code from blob storage account: {0}. Can not perform operation '{1}' because the blob '{2}' with eTag '{3}' has been modified or deleted by another process. Use the https://go.microsoft.com/fwlink/?linkid=2251550 to troubleshoot the issue. - - - Asynchronous operations are temporarily disabled. Please try again later. - - diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/BadRequestException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/BadRequestException.cs deleted file mode 100644 index 8604ac5eb8..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/BadRequestException.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -public class BadRequestException : ValidationException -{ - public BadRequestException(string message) - : base(message) - { - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/ChangeFeedLimitOutOfRangeException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/ChangeFeedLimitOutOfRangeException.cs deleted file mode 100644 index ef237c8098..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/ChangeFeedLimitOutOfRangeException.cs +++ /dev/null @@ -1,16 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Globalization; - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -public class ChangeFeedLimitOutOfRangeException : ValidationException -{ - public ChangeFeedLimitOutOfRangeException(int max) - : base(string.Format(CultureInfo.CurrentCulture, DicomCoreResource.ChangeFeedLimitOutOfRange, max)) - { - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/ConditionalExternalException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/ConditionalExternalException.cs deleted file mode 100644 index 0fcd603659..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/ConditionalExternalException.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -/// -/// base class for exceptions that can have different customer experience based on accessing system or customer provided resource -/// -public abstract class ConditionalExternalException : DicomServerException -{ - protected ConditionalExternalException(string message, Exception innerException, bool isExternal = false) - : base(message, innerException) - { - IsExternal = isExternal; - } - - public bool IsExternal { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/DataPartitionAlreadyExistsException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/DataPartitionAlreadyExistsException.cs deleted file mode 100644 index baa63ed45d..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/DataPartitionAlreadyExistsException.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Globalization; - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -/// -/// Exception that is thrown when partition name is not found -/// -public class DataPartitionAlreadyExistsException : BadRequestException -{ - public DataPartitionAlreadyExistsException() - : base(string.Format(CultureInfo.InvariantCulture, DicomCoreResource.DataPartitionAlreadyExists)) - { - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/DataPartitionsFeatureCannotBeDisabledException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/DataPartitionsFeatureCannotBeDisabledException.cs deleted file mode 100644 index 6e210ae582..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/DataPartitionsFeatureCannotBeDisabledException.cs +++ /dev/null @@ -1,20 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Globalization; - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -/// -/// Exception that is thrown when data partitions feature is disabled. -/// -public class DataPartitionsFeatureCannotBeDisabledException : InvalidOperationException -{ - public DataPartitionsFeatureCannotBeDisabledException() - : base(string.Format(CultureInfo.InvariantCulture, DicomCoreResource.DataPartitionFeatureCannotBeDisabled)) - { - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/DataPartitionsFeatureDisabledException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/DataPartitionsFeatureDisabledException.cs deleted file mode 100644 index 41a9df1896..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/DataPartitionsFeatureDisabledException.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Globalization; - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -/// -/// Exception that is thrown when data partitions feature is disabled. -/// -public class DataPartitionsFeatureDisabledException : BadRequestException -{ - public DataPartitionsFeatureDisabledException() - : base(string.Format(CultureInfo.InvariantCulture, DicomCoreResource.DataPartitionsFeatureDisabled)) - { - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/DataPartitionsMissingPartitionException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/DataPartitionsMissingPartitionException.cs deleted file mode 100644 index ba619dbc65..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/DataPartitionsMissingPartitionException.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Globalization; - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -/// -/// Exception that is thrown when partition name is missing in the route values. -/// -public class DataPartitionsMissingPartitionException : BadRequestException -{ - public DataPartitionsMissingPartitionException() - : base(string.Format(CultureInfo.InvariantCulture, DicomCoreResource.DataPartitionsMissingPartition)) - { - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/DataPartitionsNotFoundException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/DataPartitionsNotFoundException.cs deleted file mode 100644 index 4f5971a69b..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/DataPartitionsNotFoundException.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Globalization; - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -/// -/// Exception that is thrown when partition name is not found -/// -public class DataPartitionsNotFoundException : BadRequestException -{ - public DataPartitionsNotFoundException() - : base(string.Format(CultureInfo.InvariantCulture, DicomCoreResource.DataPartitionNotFound)) - { - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/DataStoreException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/DataStoreException.cs deleted file mode 100644 index 30b05113bf..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/DataStoreException.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Globalization; - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -public class DataStoreException : ConditionalExternalException -{ - public DataStoreException(Exception innerException, bool isExternal = false) - : this(isExternal ? string.Format(CultureInfo.InvariantCulture, DicomCoreResource.ExternalDataStoreOperationFailed, innerException?.Message) : DicomCoreResource.DataStoreOperationFailed, innerException, null, isExternal) - { - } - - public DataStoreException(string message, ushort? failureCode = null, bool isExternal = false) - : this(message, null, failureCode, isExternal) - { - } - - public DataStoreException(string message, Exception innerException, ushort? failureCode = null, bool isExternal = false) - : base(message, innerException, isExternal) - { - FailureCode = failureCode; - } - - public ushort? FailureCode { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/DataStoreNotReadyException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/DataStoreNotReadyException.cs deleted file mode 100644 index 7f279a7a1e..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/DataStoreNotReadyException.cs +++ /dev/null @@ -1,15 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -public class DataStoreNotReadyException : DataStoreException -{ - public DataStoreNotReadyException(string message) - : base(message) - { - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/DataStoreRequestFailedException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/DataStoreRequestFailedException.cs deleted file mode 100644 index 387019e14f..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/DataStoreRequestFailedException.cs +++ /dev/null @@ -1,50 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Globalization; -using Azure; -using Microsoft.Health.Dicom.Core.Extensions; - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -public class DataStoreRequestFailedException : ConditionalExternalException -{ - public int ResponseCode => InnerException is RequestFailedException ex ? ex.Status : 0; - - public string ErrorCode => InnerException is RequestFailedException ex ? ex.ErrorCode : null; - - public DataStoreRequestFailedException(RequestFailedException ex, bool isExternal = false) - : base( - (isExternal ? - getFormattedExternalStoreMessage(ex) - : DicomCoreResource.DataStoreOperationFailed), - ex, - isExternal) - { - } - - private static string getFormattedExternalStoreMessage(RequestFailedException ex) - { - return !string.IsNullOrEmpty(ex?.ErrorCode) - ? string.Format( - CultureInfo.InvariantCulture, - DicomCoreResource.ExternalDataStoreOperationFailed, - ex?.ErrorCode) - : GetFormattedExternalStoreMessageWithoutErrorCode(ex) - ; - } - - private static string GetFormattedExternalStoreMessageWithoutErrorCode(RequestFailedException ex) - { - if (ex.IsStorageAccountUnknownHostError()) - { - return DicomCoreResource.ExternalDataStoreHostIsUnknown; - } - - // if we do not have an error code and internal message is not "host not known", we are not familiar with the issue - // we can't just give back the exception message as it may contain sensitive information - return DicomCoreResource.ExternalDataStoreOperationFailedUnknownIssue; - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/DicomAsyncOperationDisabledException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/DicomAsyncOperationDisabledException.cs deleted file mode 100644 index 16d6e74647..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/DicomAsyncOperationDisabledException.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Globalization; - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -/// -/// Exception that is thrown when async operation is disabled. -/// -public class DicomAsyncOperationDisabledException : BadRequestException -{ - public DicomAsyncOperationDisabledException() - : base(string.Format(CultureInfo.InvariantCulture, DicomCoreResource.DicomAsyncOperationDisabled)) - { - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/DicomImageException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/DicomImageException.cs deleted file mode 100644 index 94d40f78db..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/DicomImageException.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -public class DicomImageException : DicomServerException -{ - public DicomImageException() - : base(DicomCoreResource.DicomImageConversionFailed) - { - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/DicomServerException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/DicomServerException.cs deleted file mode 100644 index e8f127661e..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/DicomServerException.cs +++ /dev/null @@ -1,25 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using Microsoft.Health.Abstractions.Exceptions; - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -/// -/// Base class for all server exceptions -/// -public abstract class DicomServerException : MicrosoftHealthException -{ - protected DicomServerException(string message) - : base(message) - { - } - - protected DicomServerException(string message, Exception innerException) - : base(message, innerException) - { - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/DicomUpdateFeatureDisabledException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/DicomUpdateFeatureDisabledException.cs deleted file mode 100644 index ffad2950c6..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/DicomUpdateFeatureDisabledException.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Globalization; - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -/// -/// Exception that is thrown when Dicom update feature is disabled. -/// -public class DicomUpdateFeatureDisabledException : BadRequestException -{ - public DicomUpdateFeatureDisabledException() - : base(string.Format(CultureInfo.InvariantCulture, DicomCoreResource.DicomUpdateFeatureDisabled)) - { - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/ElementValidationException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/ElementValidationException.cs deleted file mode 100644 index 237381da79..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/ElementValidationException.cs +++ /dev/null @@ -1,35 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Globalization; -using EnsureThat; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Validation; - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -public class ElementValidationException : ValidationException -{ - public ElementValidationException(string name, DicomVR vr, ValidationErrorCode errorCode) - : this(name, vr, errorCode, errorCode.GetMessage()) - { } - - public ElementValidationException(string name, DicomVR vr, ValidationErrorCode errorCode, string message) - : base(message) - { - Name = EnsureArg.IsNotNull(name, nameof(name)); - VR = EnsureArg.IsNotNull(vr, nameof(vr)); - ErrorCode = EnsureArg.EnumIsDefined(errorCode, nameof(errorCode)); - } - - public string Name { get; } - - public DicomVR VR { get; } - - public ValidationErrorCode ErrorCode { get; } - - public override string Message => string.Format(CultureInfo.CurrentCulture, DicomCoreResource.DicomElementValidationFailed, Name, VR.Code, base.Message); -} diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/ExistingOperationException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/ExistingOperationException.cs deleted file mode 100644 index b0b832a978..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/ExistingOperationException.cs +++ /dev/null @@ -1,39 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using EnsureThat; -using System.Globalization; -using Microsoft.Health.Operations; - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -/// -/// The exception that is thrown when a new operation is submitted while one is already active. -/// -public class ExistingOperationException : Exception -{ - /// - /// Gets the reference to the existing operation. - /// - /// The for the existing operation, if specified. - public OperationReference ExistingOperation { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The operation reference for the existing operation. - /// Type of operation (eg: update, re-index) - /// is . - public ExistingOperationException(OperationReference operation, string operationType) - : base(string.Format( - CultureInfo.CurrentCulture, - DicomCoreResource.ExistingOperation, - operationType, - EnsureArg.IsNotNull(operation, nameof(operation)).Id.ToString(OperationId.FormatSpecifier))) - { - ExistingOperation = operation; - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/ExtendedQueryTagBusyException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/ExtendedQueryTagBusyException.cs deleted file mode 100644 index 34581ad663..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/ExtendedQueryTagBusyException.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -/// -/// Exception thrown when extended query tag is busy. (e.g: trying to delete extended query tag when it's reindexing) -/// -public class ExtendedQueryTagBusyException : ValidationException -{ - public ExtendedQueryTagBusyException(string message) - : base(message) - { - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/ExtendedQueryTagEntryValidationException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/ExtendedQueryTagEntryValidationException.cs deleted file mode 100644 index f10dd00f36..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/ExtendedQueryTagEntryValidationException.cs +++ /dev/null @@ -1,21 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -public class ExtendedQueryTagEntryValidationException : ValidationException -{ - public ExtendedQueryTagEntryValidationException(string message) - : base(message) - { - } - - public ExtendedQueryTagEntryValidationException(string message, Exception innerException) - : base(message, innerException) - { - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/ExtendedQueryTagNotFoundException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/ExtendedQueryTagNotFoundException.cs deleted file mode 100644 index 345433806c..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/ExtendedQueryTagNotFoundException.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -/// -/// Exception thrown when extended query tags don't exist or extended query tag with given tag path does not exist. -/// -public class ExtendedQueryTagNotFoundException : ResourceNotFoundException -{ - public ExtendedQueryTagNotFoundException(string message) - : base(message) - { - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/ExtendedQueryTagsAlreadyExistsException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/ExtendedQueryTagsAlreadyExistsException.cs deleted file mode 100644 index bc72bc2117..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/ExtendedQueryTagsAlreadyExistsException.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -/// -/// Exception thrown when the extended query tag already exists. -/// -public class ExtendedQueryTagsAlreadyExistsException : DicomServerException -{ - public ExtendedQueryTagsAlreadyExistsException() - : base(DicomCoreResource.ExtendedQueryTagsAlreadyExists) - { - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/ExtendedQueryTagsExceedsMaxAllowedCount.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/ExtendedQueryTagsExceedsMaxAllowedCount.cs deleted file mode 100644 index e1e786da93..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/ExtendedQueryTagsExceedsMaxAllowedCount.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Globalization; - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -/// -/// Exception thrown when the extended query tags exceeds max allowed count. -/// -public class ExtendedQueryTagsExceedsMaxAllowedCountException : DicomServerException -{ - public ExtendedQueryTagsExceedsMaxAllowedCountException(int maxAllowedCount) - : base(string.Format(CultureInfo.InvariantCulture, DicomCoreResource.ExtendedQueryTagsExceedsMaxAllowedCount, maxAllowedCount)) - { - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/ExtendedQueryTagsOutOfDateException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/ExtendedQueryTagsOutOfDateException.cs deleted file mode 100644 index c28eeb4642..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/ExtendedQueryTagsOutOfDateException.cs +++ /dev/null @@ -1,18 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -/// -/// Exception thrown when the client is missing one or more extended query tags from the server. -/// -public class ExtendedQueryTagsOutOfDateException - : DicomServerException -{ - public ExtendedQueryTagsOutOfDateException() - : base(DicomCoreResource.ExtendedQueryTagsOutOfDate) - { - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/FrameNotFoundException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/FrameNotFoundException.cs deleted file mode 100644 index 49bd18947f..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/FrameNotFoundException.cs +++ /dev/null @@ -1,21 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -public class FrameNotFoundException : ResourceNotFoundException -{ - public FrameNotFoundException() - : base(DicomCoreResource.FrameNotFound) - { - } - - public FrameNotFoundException(Exception innerException) - : base(DicomCoreResource.FrameNotFound, innerException) - { - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/InstanceAlreadyExistsException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/InstanceAlreadyExistsException.cs deleted file mode 100644 index 4edb7b410a..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/InstanceAlreadyExistsException.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -/// -/// Exception thrown when the DICOM instance already exists. -/// -public class InstanceAlreadyExistsException : DicomServerException -{ - public InstanceAlreadyExistsException() - : base(DicomCoreResource.InstanceAlreadyExists) - { - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/InstanceNotFoundException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/InstanceNotFoundException.cs deleted file mode 100644 index 263af41309..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/InstanceNotFoundException.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -public class InstanceNotFoundException : ResourceNotFoundException -{ - public InstanceNotFoundException() - : base(DicomCoreResource.InstanceNotFound) - { - } - - public InstanceNotFoundException(string message) - : base(message) - { - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/InvalidChangeFeedOffsetException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/InvalidChangeFeedOffsetException.cs deleted file mode 100644 index 8738ff2ed9..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/InvalidChangeFeedOffsetException.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -public class InvalidChangeFeedOffsetException : ValidationException -{ - public InvalidChangeFeedOffsetException() - : base(DicomCoreResource.ChangeFeedOffsetCannotBeNegative) - { - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/InvalidExtendedQueryTagPathException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/InvalidExtendedQueryTagPathException.cs deleted file mode 100644 index d060390aea..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/InvalidExtendedQueryTagPathException.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -/// -/// Exception thrown when extended query tag path is invalid. -/// -public class InvalidExtendedQueryTagPathException : ValidationException -{ - public InvalidExtendedQueryTagPathException(string message) - : base(message) - { - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/InvalidIdentifierException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/InvalidIdentifierException.cs deleted file mode 100644 index f452b87aa2..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/InvalidIdentifierException.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Features.Validation; - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -public class InvalidIdentifierException : ElementValidationException -{ - public InvalidIdentifierException(string name) - : base(name, DicomVR.UI, ValidationErrorCode.UidIsInvalid, DicomCoreResource.ErrorMessageUidIsInvalid) - { - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/InvalidInstanceException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/InvalidInstanceException.cs deleted file mode 100644 index 068acb7918..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/InvalidInstanceException.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -/// -/// Exception thrown when the DICOM instance is invalid. -/// -public class InvalidInstanceException : ValidationException -{ - public InvalidInstanceException(string message) - : base(message) - { - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/InvalidMultipartRequestException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/InvalidMultipartRequestException.cs deleted file mode 100644 index 5b2fd2ee8b..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/InvalidMultipartRequestException.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -public class InvalidMultipartRequestException : BadRequestException -{ - public InvalidMultipartRequestException(string message) - : base(message) - { - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/InvalidPartitionNameException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/InvalidPartitionNameException.cs deleted file mode 100644 index da8debcf92..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/InvalidPartitionNameException.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -/// -/// Exception thrown when the partition name is invalid. -/// -public class InvalidPartitionNameException : ValidationException -{ - public InvalidPartitionNameException() - : base(DicomCoreResource.InvalidPartitionName) - { - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/InvalidQueryStringException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/InvalidQueryStringException.cs deleted file mode 100644 index 0b6e80cc29..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/InvalidQueryStringException.cs +++ /dev/null @@ -1,16 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Globalization; - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -public class InvalidQueryStringException : ValidationException -{ - public InvalidQueryStringException() - : base(string.Format(CultureInfo.InvariantCulture, DicomCoreResource.InvalidQueryString)) - { - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/InvalidQueryStringValuesException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/InvalidQueryStringValuesException.cs deleted file mode 100644 index cc156d5c22..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/InvalidQueryStringValuesException.cs +++ /dev/null @@ -1,16 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Globalization; - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -public class InvalidQueryStringValuesException : ValidationException -{ - public InvalidQueryStringValuesException(string key, string error) - : base(string.Format(CultureInfo.InvariantCulture, DicomCoreResource.InvalidQueryStringValue, key, error)) - { - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/ItemNotFoundException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/ItemNotFoundException.cs deleted file mode 100644 index 92d7db56f9..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/ItemNotFoundException.cs +++ /dev/null @@ -1,16 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -public class ItemNotFoundException : DicomServerException -{ - public ItemNotFoundException(Exception innerException) - : base(DicomCoreResource.ItemNotFound, innerException) - { - } -} \ No newline at end of file diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/NotAcceptableException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/NotAcceptableException.cs deleted file mode 100644 index 939e6b93bf..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/NotAcceptableException.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -public class NotAcceptableException : DicomServerException -{ - public NotAcceptableException(string message) - : base(message) - { - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/NotSupportedException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/NotSupportedException.cs deleted file mode 100644 index b6e7559bf5..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/NotSupportedException.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -public class NotSupportedException : DicomServerException -{ - public NotSupportedException(string message) - : base(message) - { - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/PayloadTooLargeException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/PayloadTooLargeException.cs deleted file mode 100644 index d7ac4cac5a..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/PayloadTooLargeException.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Globalization; - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -/// -/// The incoming payload exceeds configured limits. -/// -public class PayloadTooLargeException : DicomServerException -{ - public PayloadTooLargeException(long maxAllowedLength) - : base(string.Format(CultureInfo.InvariantCulture, DicomCoreResource.RequestLengthLimitExceeded, maxAllowedLength)) - { - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/PendingInstanceException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/PendingInstanceException.cs deleted file mode 100644 index 44179c40a6..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/PendingInstanceException.cs +++ /dev/null @@ -1,15 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -/// -/// Exception thrown when there is already a pending DICOM instance being created. -/// -public class PendingInstanceException : Exception -{ -} diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/ResourceNotFoundException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/ResourceNotFoundException.cs deleted file mode 100644 index 8a74f197ac..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/ResourceNotFoundException.cs +++ /dev/null @@ -1,21 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -public class ResourceNotFoundException : DicomServerException -{ - public ResourceNotFoundException(string message) - : base(message) - { - } - - public ResourceNotFoundException(string message, Exception innerException) - : base(message, innerException) - { - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/SeriesNotFoundException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/SeriesNotFoundException.cs deleted file mode 100644 index dc8b4bdf67..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/SeriesNotFoundException.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -public class SeriesNotFoundException : ResourceNotFoundException -{ - public SeriesNotFoundException() - : base(DicomCoreResource.SeriesNotFound) - { - } - - public SeriesNotFoundException(string message) - : base(message) - { - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/SinkInitializationFailureException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/SinkInitializationFailureException.cs deleted file mode 100644 index ed0fbb8d22..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/SinkInitializationFailureException.cs +++ /dev/null @@ -1,37 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using Microsoft.Health.Dicom.Core.Features.Export; - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -/// -/// Represents a failure to initialize an instance of . -/// -public class SinkInitializationFailureException : ValidationException -{ - /// - /// Initializes a new instance of the class - /// with a specified error message. - /// - /// The message that describes the error. - public SinkInitializationFailureException(string message) - : base(message) - { } - - /// - /// Initializes a new instance of the class - /// with a specified error message and a reference to the inner exception that is the cause of this exception. - /// - /// The message that describes the error. - /// - /// The exception that is the cause of the current exception, or - /// if no inner exception is specified. - /// - public SinkInitializationFailureException(string message, Exception innerException) - : base(message, innerException) - { } -} diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/StudyNotFoundException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/StudyNotFoundException.cs deleted file mode 100644 index 34488dbd5a..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/StudyNotFoundException.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -public class StudyNotFoundException : ResourceNotFoundException -{ - public StudyNotFoundException() - : base(DicomCoreResource.StudyNotFound) - { - } - - public StudyNotFoundException(string message) - : base(message) - { - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/TranscodingException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/TranscodingException.cs deleted file mode 100644 index e4e4061fc7..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/TranscodingException.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -public class TranscodingException : DicomServerException -{ - public TranscodingException() - : base(DicomCoreResource.UnsupportedTranscoding) - { - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/UnauthorizedDicomActionException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/UnauthorizedDicomActionException.cs deleted file mode 100644 index b05abf72c4..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/UnauthorizedDicomActionException.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Microsoft.Health.Dicom.Core.Features.Security; - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -public class UnauthorizedDicomActionException : DicomServerException -{ - public UnauthorizedDicomActionException(DataActions expectedDataActions) - : base(DicomCoreResource.Forbidden) - { - ExpectedDataActions = expectedDataActions; - } - - public DataActions ExpectedDataActions { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/ValidationException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/ValidationException.cs deleted file mode 100644 index 0b1db6c307..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/ValidationException.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -/// -/// Base class for all client input validation exceptions. -/// -public abstract class ValidationException : DicomServerException -{ - protected ValidationException(string message) - : base(message) - { - } - - protected ValidationException(string message, Exception innerException) - : base(message, innerException) - { - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/WorkitemAlreadyExistsException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/WorkitemAlreadyExistsException.cs deleted file mode 100644 index 5fbc8d9991..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/WorkitemAlreadyExistsException.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -/// -/// Exception thrown when the Workitem instance already exists. -/// -public class WorkitemAlreadyExistsException : DicomServerException -{ - public WorkitemAlreadyExistsException() - : base(DicomCoreResource.WorkitemInstanceAlreadyExists) - { - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/WorkitemNotFoundException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/WorkitemNotFoundException.cs deleted file mode 100644 index 32b017db90..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/WorkitemNotFoundException.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -/// -/// Exception thrown when the Workitem instance is not found. -/// -public class WorkitemNotFoundException : DicomServerException -{ - public WorkitemNotFoundException() - : base(DicomCoreResource.WorkitemInstanceNotFound) - { - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Exceptions/WorkitemUpdateNotAllowedException.cs b/src/Microsoft.Health.Dicom.Core/Exceptions/WorkitemUpdateNotAllowedException.cs deleted file mode 100644 index 627a917b41..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Exceptions/WorkitemUpdateNotAllowedException.cs +++ /dev/null @@ -1,16 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Globalization; - -namespace Microsoft.Health.Dicom.Core.Exceptions; - -public sealed class WorkitemUpdateNotAllowedException : DicomServerException -{ - public WorkitemUpdateNotAllowedException(string procedureStepState) - : base(string.Format(CultureInfo.CurrentCulture, DicomCoreResource.WorkitemUpdateIsNotAllowed, procedureStepState)) - { - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Extensions/AzureStorageErrorExtensions.cs b/src/Microsoft.Health.Dicom.Core/Extensions/AzureStorageErrorExtensions.cs deleted file mode 100644 index 9ca6447739..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Extensions/AzureStorageErrorExtensions.cs +++ /dev/null @@ -1,20 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Linq; - -namespace Microsoft.Health.Dicom.Core.Extensions; - -internal static class AzureStorageErrorExtensions -{ - public static bool IsStorageAccountUnknownHostError(this Exception exception) - { - return exception.Message.Contains("No such host is known", StringComparison.OrdinalIgnoreCase) || - exception.Message.Contains("Name or service not known", StringComparison.OrdinalIgnoreCase) || - (exception is AggregateException ag && ag.InnerExceptions.All(e => e.Message.Contains("No such host is known", StringComparison.OrdinalIgnoreCase)) || - (exception is AggregateException agex && agex.InnerExceptions.All(e => e.Message.Contains("Name or service not known", StringComparison.OrdinalIgnoreCase)))); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Extensions/DicomDatasetExtensions.cs b/src/Microsoft.Health.Dicom.Core/Extensions/DicomDatasetExtensions.cs deleted file mode 100644 index a1648a570d..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Extensions/DicomDatasetExtensions.cs +++ /dev/null @@ -1,803 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Linq; -using EnsureThat; -using FellowOakDicom; -using FellowOakDicom.IO.Writer; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Core.Features.Store; -using Microsoft.Health.Dicom.Core.Features.Validation; -using Microsoft.Health.Dicom.Core.Features.Workitem; -using Microsoft.Health.Dicom.Core.Features.Workitem.Model; -using Microsoft.Health.Dicom.Core.Models; - -namespace Microsoft.Health.Dicom.Core.Extensions; - -/// -/// Extension methods for . -/// -public static class DicomDatasetExtensions -{ - private static readonly HashSet DicomBulkDataVr = new HashSet - { - DicomVR.OB, - DicomVR.OD, - DicomVR.OF, - DicomVR.OL, - DicomVR.OV, - DicomVR.OW, - DicomVR.UN, - }; - - private const string DateFormatDA = "yyyyMMdd"; - - private static readonly string[] DateTimeFormatsDT = - { - "yyyyMMddHHmmss.FFFFFFzzz", - "yyyyMMddHHmmsszzz", - "yyyyMMddHHmmzzz", - "yyyyMMddHHzzz", - "yyyyMMddzzz", - "yyyyMMzzz", - "yyyyzzz", - "yyyyMMddHHmmss.FFFFFF", - "yyyyMMddHHmmss", - "yyyyMMddHHmm", - "yyyyMMddHH", - "yyyyMMdd", - "yyyyMM", - "yyyy" - }; - - private static readonly string[] DateTimeOffsetFormats = new string[] - { - "hhmm", - "\\+hhmm", - "\\-hhmm" - }; - - private static readonly HashSet ByteArrayVRs = new HashSet - { - "OB", - "UN" - }; - - private static readonly HashSet DecimalVRs = new HashSet { "DS" }; - - private static readonly HashSet DoubleVRs = new HashSet { "FD" }; - - private static readonly HashSet FloatVRs = new HashSet { "FL" }; - - private static readonly HashSet IntVRs = new HashSet - { - "IS", - "SL" - }; - - private static readonly HashSet LongVRs = new HashSet { "SV" }; - - private static readonly HashSet ShortVRs = new HashSet { "SS" }; - - private static readonly HashSet StringVRs = new HashSet - { - "AE", - "AS", - "CS", - "DA", - "DT", - "LO", - "LT", - "PN", - "SH", - "ST", - "TM", - "UC", - "UI", - "UR", - "UT" - }; - - private static readonly HashSet UIntVRs = new HashSet { "UL" }; - - private static readonly HashSet ULongVRs = new HashSet { "UV" }; - - private static readonly HashSet UShortVRs = new HashSet { "US" }; - - private static readonly HashSet MandatoryRequirementCodes = new HashSet - { - RequirementCode.OneOne, - RequirementCode.TwoOne, - RequirementCode.TwoTwo - }; - - private static readonly HashSet NonZeroLengthRequirementCodes = new HashSet - { - RequirementCode.OneOne, - RequirementCode.ThreeOne, - RequirementCode.ThreeTwo, - RequirementCode.ThreeThree, - RequirementCode.OneCOne, - RequirementCode.OneCOneC, - RequirementCode.OneCTwo - }; - - /// - /// Gets a single value if the value exists; otherwise the default value for the type . - /// - /// The value type. - /// The dataset to get the VR value from. - /// The DICOM tag. - /// Expected VR of the element. - /// If expectedVR is provided, and not match, will return default - /// The value if the value exists; otherwise, the default value for the type . - public static T GetFirstValueOrDefault(this DicomDataset dicomDataset, DicomTag dicomTag, DicomVR expectedVR = null) - { - EnsureArg.IsNotNull(dicomDataset, nameof(dicomDataset)); - DicomElement element = dicomDataset.GetDicomItem(dicomTag); - if (element == null) - { - return default; - } - - // If VR doesn't match, return default(T) - if (expectedVR != null && element.ValueRepresentation != expectedVR) - { - return default; - } - - return element.GetFirstValueOrDefault(); - } - - /// - /// Gets the DA VR value as . - /// - /// The dataset to get the VR value from. - /// The DICOM tag. - /// Expected VR of the element. - /// If expectedVR is provided, and not match, will return null. - /// An instance of if the value exists and comforms to the DA format; otherwise null. - public static DateTime? GetStringDateAsDate(this DicomDataset dicomDataset, DicomTag dicomTag, DicomVR expectedVR = null) - { - EnsureArg.IsNotNull(dicomDataset, nameof(dicomDataset)); - string stringDate = dicomDataset.GetFirstValueOrDefault(dicomTag, expectedVR: expectedVR); - return DateTime.TryParseExact(stringDate, DateFormatDA, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime result) ? result : null; - } - - /// - /// Gets the DT VR values as literal and UTC . - /// If offset is not provided in the value and in the TimezoneOffsetFromUTC fields, UTC DateTime will be null. - /// - /// The dataset to get the VR value from. - /// The DICOM tag. - /// Expected VR of the element. - /// If expectedVR is provided, and not match, will return null. - /// A of ( , ) representing literal date time and Utc date time respectively is returned. If value does not exist or does not conform to the DT format, null is returned for DateTimes. If offset information is not present, null is returned for Item2 i.e. Utc Date Time. - public static Tuple GetStringDateTimeAsLiteralAndUtcDateTimes(this DicomDataset dicomDataset, DicomTag dicomTag, DicomVR expectedVR = null) - { - EnsureArg.IsNotNull(dicomDataset, nameof(dicomDataset)); - string stringDateTime = dicomDataset.GetFirstValueOrDefault(dicomTag, expectedVR: expectedVR); - - if (string.IsNullOrEmpty(stringDateTime)) - { - // If no valid data found, return null values in tuple. - return new Tuple(null, null); - } - - Tuple result = new Tuple(null, null); - - // Parsing as DateTime such that we can know the DateTimeKind. - // If offset is present in the value, DateTimeKind is Local, else it is Unspecified. - // Ideally would like to work with just DateTimeOffsets to avoid parsing multiple times, but DateTimeKind does not work as expected with DateTimeOffset. - if (DateTime.TryParseExact(stringDateTime, DateTimeFormatsDT, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime dateTime)) - { - // Using DateTimeStyles.AssumeUniversal here such that when applying offset, local timezone (offset) is not taken into account. - DateTimeOffset.TryParseExact(stringDateTime, DateTimeFormatsDT, null, DateTimeStyles.AssumeUniversal, out DateTimeOffset dateTimeOffset); - - // Unspecified means that the offset is not present in the value. - if (dateTime.Kind == DateTimeKind.Unspecified) - { - // Check if offset is present in TimezoneOffsetFromUTC - TimeSpan? offset = dicomDataset.GetTimezoneOffsetFromUtcAsTimeSpan(); - - if (offset != null) - { - // If we can parse the offset, use that offset to calculate UTC Date Time. - result = new Tuple(dateTimeOffset.DateTime, dateTimeOffset.ToOffset(offset.Value).DateTime); - } - else - { - // If either offset is not present or could not be parsed, UTC should be null. - result = new Tuple(dateTimeOffset.DateTime, null); - } - } - else - { - // If offset is present in the value, it can simply be converted to UTC - result = new Tuple(dateTimeOffset.DateTime, dateTimeOffset.UtcDateTime); - } - } - - return result; - } - - /// - /// Get TimezoneOffsetFromUTC value as TimeSpan. - /// - /// The dataset to get the TimezoneOffsetFromUTC value from. - /// An instance of . If value is not found or could not be parsed, null is returned. - private static TimeSpan? GetTimezoneOffsetFromUtcAsTimeSpan(this DicomDataset dicomDataset) - { - // Cannot parse it directly as TimeSpan as the offset needs to follow specific formats. - string offset = dicomDataset.GetFirstValueOrDefault(DicomTag.TimezoneOffsetFromUTC, expectedVR: DicomVR.SH); - - if (!string.IsNullOrEmpty(offset)) - { - TimeSpan timeSpan; - - // Need to look at offset string to figure out positive or negative offset - // as timespan ParseExact does not support negative offsets by default. - // Applying TimeSpanStyles.AssumeNegative is the only documented way to handle negative offsets for this method. - bool isSuccess = offset[0] == '-' ? - TimeSpan.TryParseExact(offset, DateTimeOffsetFormats, CultureInfo.InvariantCulture, TimeSpanStyles.AssumeNegative, out timeSpan) : - TimeSpan.TryParseExact(offset, DateTimeOffsetFormats, CultureInfo.InvariantCulture, out timeSpan); - - if (isSuccess) - { - return timeSpan; - } - } - - return null; - } - - /// - /// Gets the TM VR value as . - /// - /// The dataset to get the VR value from. - /// The DICOM tag. - /// Expected VR of the element. - /// If expectedVR is provided, and not match, will return null. - /// A long value representing the ticks if the value exists and conforms to the TM format; othewise null. - [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Will reevaluate upon inspection of possible exceptions.")] - public static long? GetStringTimeAsLong(this DicomDataset dicomDataset, DicomTag dicomTag, DicomVR expectedVR = null) - { - EnsureArg.IsNotNull(dicomDataset, nameof(dicomDataset)); - long? result = null; - - try - { - result = dicomDataset.GetFirstValueOrDefault(dicomTag, expectedVR: expectedVR).Ticks; - } - catch (Exception) - { - result = null; - } - - // If parsing fails for Time, a default value of 0 is returned. Since expected outcome is null, testing here and returning expected result. - return result == 0 ? null : result; - } - - /// - /// Creates a new copy of DICOM dataset with items of VR types considered to be bulk data removed. - /// - /// The DICOM dataset. - /// A copy of the with items of VR types considered to be bulk data removed. - public static DicomDataset CopyWithoutBulkDataItems(this DicomDataset dicomDataset) - { - EnsureArg.IsNotNull(dicomDataset, nameof(dicomDataset)); - - return CopyDicomDatasetWithoutBulkDataItems(dicomDataset); - - static DicomDataset CopyDicomDatasetWithoutBulkDataItems(DicomDataset dicomDatasetToCopy) - { - return new DicomDataset(dicomDatasetToCopy - .Select(dicomItem => - { - if (DicomBulkDataVr.Contains(dicomItem.ValueRepresentation)) - { - // If the VR is bulk data type, return null so it can be filtered out later. - return null; - } - else if (dicomItem.ValueRepresentation == DicomVR.SQ) - { - // If the VR is sequence, then process each item within the sequence. - DicomSequence sequenceToCopy = (DicomSequence)dicomItem; - - return new DicomSequence( - sequenceToCopy.Tag, - sequenceToCopy.Select(itemToCopy => itemToCopy.CopyWithoutBulkDataItems()).ToArray()); - } - else - { - // The VR is not bulk data, return it. - return dicomItem; - } - }) - .Where(dicomItem => dicomItem != null)); - } - } - - /// - /// Creates an instance of from . - /// - /// The DICOM dataset to get the identifiers from. - /// Data Partition entry - /// An instance of representing the . - public static InstanceIdentifier ToInstanceIdentifier(this DicomDataset dicomDataset, Partition partition) - { - EnsureArg.IsNotNull(dicomDataset, nameof(dicomDataset)); - - // Note: Here we 'GetSingleValueOrDefault' and let the constructor validate the identifier. - return new InstanceIdentifier( - dicomDataset.GetSingleValueOrDefault(DicomTag.StudyInstanceUID, string.Empty), - dicomDataset.GetSingleValueOrDefault(DicomTag.SeriesInstanceUID, string.Empty), - dicomDataset.GetSingleValueOrDefault(DicomTag.SOPInstanceUID, string.Empty), - partition); - } - - /// - /// Creates an instance of from . - /// - /// The DICOM dataset to get the identifiers from. - /// The version. - /// Data Partition entry - /// An instance of representing the . - public static VersionedInstanceIdentifier ToVersionedInstanceIdentifier(this DicomDataset dicomDataset, long version, Partition partition) - { - EnsureArg.IsNotNull(dicomDataset, nameof(dicomDataset)); - - // Note: Here we 'GetSingleValueOrDefault' and let the constructor validate the identifier. - return new VersionedInstanceIdentifier( - dicomDataset.GetSingleValueOrDefault(DicomTag.StudyInstanceUID, string.Empty), - dicomDataset.GetSingleValueOrDefault(DicomTag.SeriesInstanceUID, string.Empty), - dicomDataset.GetSingleValueOrDefault(DicomTag.SOPInstanceUID, string.Empty), - version, - partition); - } - - /// - /// Adds value to the if is not null. - /// - /// The value type. - /// The dataset to add value to. - /// The DICOM tag. - /// The value to add. - public static void AddValueIfNotNull(this DicomDataset dicomDataset, DicomTag dicomTag, T value) - where T : class - { - EnsureArg.IsNotNull(dicomDataset, nameof(dicomDataset)); - EnsureArg.IsNotNull(dicomTag, nameof(dicomTag)); - - if (value != null) - { - dicomDataset.Add(dicomTag, value); - } - } - - /// - /// Validate query tag in Dicom dataset. - /// - /// The dicom dataset. - /// The query tag. - /// The minimum validator. - public static ValidationWarnings ValidateQueryTag(this DicomDataset dataset, QueryTag queryTag, IElementMinimumValidator minimumValidator) - { - EnsureArg.IsNotNull(dataset, nameof(dataset)); - EnsureArg.IsNotNull(queryTag, nameof(queryTag)); - - return dataset.ValidateDicomTag(queryTag.Tag, minimumValidator); - } - - /// - /// Validate dicom tag in Dicom dataset. - /// - /// The dicom dataset. - /// The dicom tag being validated. - /// The minimum validator. - /// Style of validation to enforce on running rules - public static ValidationWarnings ValidateDicomTag(this DicomDataset dataset, DicomTag dicomTag, IElementMinimumValidator minimumValidator, ValidationLevel validationLevel = ValidationLevel.Strict) - { - EnsureArg.IsNotNull(dataset, nameof(dataset)); - EnsureArg.IsNotNull(dicomTag, nameof(dicomTag)); - EnsureArg.IsNotNull(minimumValidator, nameof(minimumValidator)); - DicomElement dicomElement = dataset.GetDicomItem(dicomTag); - - ValidationWarnings warning = ValidationWarnings.None; - if (dicomElement != null) - { - if (dicomElement.ValueRepresentation != dicomTag.GetDefaultVR()) - { - string name = dicomElement.Tag.GetFriendlyName(); - DicomVR actualVR = dicomElement.ValueRepresentation; - throw new ElementValidationException( - name, - actualVR, - ValidationErrorCode.UnexpectedVR, - string.Format(CultureInfo.InvariantCulture, DicomCoreResource.ErrorMessageUnexpectedVR, name, dicomTag.GetDefaultVR(), actualVR)); - } - - if (dicomElement.Count > 1) - { - warning |= ValidationWarnings.IndexedDicomTagHasMultipleValues; - } - - minimumValidator.Validate(dicomElement, validationLevel); - } - return warning; - } - - /// - /// Gets DicomDatasets that matches a list of tags reprenting a tag path. - /// - /// The DicomDataset to be traversed. - /// The Dicom tags modelling the path. - /// Lists of DicomDataset that matches the list of tags. - public static IEnumerable GetSequencePathValues(this DicomDataset dataset, ReadOnlyCollection dicomTags) - { - EnsureArg.IsNotNull(dataset, nameof(dataset)); - EnsureArg.IsNotNull(dicomTags, nameof(dicomTags)); - - if (dicomTags.Count != 2) - { - throw new ElementValidationException(string.Join(", ", dicomTags.Select(x => x.GetPath())), DicomVR.SQ, ValidationErrorCode.NestedSequence); - } - - var foundSequence = dataset.GetSequence(dicomTags[0]); - if (foundSequence != null) - { - foreach (var childDataset in foundSequence.Items) - { - var item = childDataset.GetDicomItem(dicomTags[1]); - - if (item != null) - { - yield return new DicomDataset(item); - } - } - } - } - - public static void ValidateAllRequirements(this DicomDataset dataset, IReadOnlyCollection requirements) - { - if (requirements == null) - { - return; - } - - foreach (RequirementDetail requirement in requirements) - { - dataset.ValidateRequirement(requirement.DicomTag, requirement.RequirementCode); - - // If no sequence requirements are present, move on to the next tag. - if (requirement.SequenceRequirements == null) - { - continue; - } - - // If current tag is not allowed, no action needed for its potential children. - if (requirement.RequirementCode == RequirementCode.NotAllowed) - { - continue; - } - - bool isMandatory = MandatoryRequirementCodes.Contains(requirement.RequirementCode); - bool isNonZero = NonZeroLengthRequirementCodes.Contains(requirement.RequirementCode); - bool hasChildren = dataset.Contains(requirement.DicomTag) && dataset.GetValueCount(requirement.DicomTag) > 0; - - // Validate sequence only if - // 1. Parent is mandatory and is non-zero, means it has to have children. OR - // 2. Parent contains children regardless of being mandatory or not. - if ((isMandatory && isNonZero) || - hasChildren) - { - dataset.ValidateSequence(requirement.DicomTag, requirement.SequenceRequirements); - } - } - } - - private static void ValidateSequence(this DicomDataset dataset, DicomTag sequenceTag, IReadOnlyCollection requirements) - { - if (requirements == null || requirements.Count == 0 || !dataset.TryGetSequence(sequenceTag, out DicomSequence sequence) || sequence.Items.Count == 0) - { - return; - } - - foreach (DicomDataset sequenceDataset in sequence.Items) - { - foreach (RequirementDetail requirement in requirements) - { - sequenceDataset.ValidateRequirement(requirement.DicomTag, requirement.RequirementCode); - - if (requirement.SequenceRequirements != null) - { - sequenceDataset.ValidateSequence(requirement.DicomTag, requirement.SequenceRequirements); - } - } - } - } - - /// - /// Validate whether a dataset meets the service class user (SCU) and service class provider (SCP) requirements for a given attribute. - /// Dicom 3.4.5.4.2.1 - /// - /// The dataset to validate. - /// The tag for the attribute that is required. - /// The requirement code expressed as an enum. - public static void ValidateRequirement(this DicomDataset dataset, DicomTag tag, RequirementCode requirement) - { - EnsureArg.IsNotNull(dataset, nameof(dataset)); - EnsureArg.IsNotNull(tag, nameof(tag)); - - dataset.ValidateRequiredAttribute(tag, requirement); - } - - public static void ValidateRequirement( - this DicomDataset dataset, - DicomTag tag, - ProcedureStepState targetProcedureStepState, - FinalStateRequirementCode requirement, - Func requirementCondition = default) - { - EnsureArg.IsNotNull(dataset, nameof(dataset)); - EnsureArg.IsNotNull(tag, nameof(tag)); - - var predicate = (requirementCondition == default) ? (ds, tag) => false : requirementCondition; - - if (targetProcedureStepState != ProcedureStepState.Completed && - targetProcedureStepState != ProcedureStepState.Canceled) - { - return; - } - - switch (requirement) - { - case FinalStateRequirementCode.R: - dataset.ValidateRequiredAttribute(tag); - break; - case FinalStateRequirementCode.RC: - if (predicate(dataset, tag)) - { - dataset.ValidateRequiredAttribute(tag); - } - - break; - case FinalStateRequirementCode.P: - if (ProcedureStepState.Completed == targetProcedureStepState) - { - dataset.ValidateRequiredAttribute(tag); - } - - break; - case FinalStateRequirementCode.X: - if (ProcedureStepState.Canceled == targetProcedureStepState) - { - dataset.ValidateRequiredAttribute(tag); - } - - break; - case FinalStateRequirementCode.O: - break; - } - } - - /// - /// Returns a dicom item if it matches a large object size criteria. - /// - /// - /// - /// - /// - /// dicom item - public static bool TryGetLargeDicomItem( - this DicomDataset dataset, - int minLargeObjectsizeInBytes, - int maxLargeObjectsizeInBytes, - out DicomItem largeDicomItem) - { - EnsureArg.IsNotNull(dataset, nameof(dataset)); - EnsureArg.IsGte(minLargeObjectsizeInBytes, 0, nameof(minLargeObjectsizeInBytes)); - EnsureArg.IsGte(maxLargeObjectsizeInBytes, 0, nameof(maxLargeObjectsizeInBytes)); - - long totalSize = 0; - largeDicomItem = null; - - var calculator = new DicomWriteLengthCalculator(dataset.InternalTransferSyntax, DicomWriteOptions.Default); - - foreach (var item in dataset) - { - long length = calculator.Calculate(item); - if (length >= minLargeObjectsizeInBytes) - { - largeDicomItem = item; - return true; - } - - totalSize += length; - - // If the total size is greater than the max block size, we will return the last dicom item - if (totalSize >= maxLargeObjectsizeInBytes) - { - largeDicomItem = item; - return true; - } - } - - return false; - } - - private static void ValidateRequiredAttribute(this DicomDataset dataset, DicomTag tag, RequirementCode requirementCode) - { - EnsureArg.IsNotNull(dataset, nameof(dataset)); - EnsureArg.IsNotNull(tag, nameof(tag)); - - switch (requirementCode) - { - case RequirementCode.MustBeEmpty: - dataset.ValidateEmptyValue(tag); - break; - case RequirementCode.NotAllowed: - dataset.ValidateNotPresent(tag); - break; - default: - dataset.ValidateRequiredAttribute(tag, MandatoryRequirementCodes.Contains(requirementCode), NonZeroLengthRequirementCodes.Contains(requirementCode)); - break; - } - } - - private static void ValidateRequiredAttribute(this DicomDataset dataset, DicomTag tag, bool isMandatory = true, bool isNonZero = true) - { - EnsureArg.IsNotNull(dataset, nameof(dataset)); - EnsureArg.IsNotNull(tag, nameof(tag)); - - if (isMandatory && !dataset.Contains(tag)) - { - throw new DatasetValidationException( - FailureReasonCodes.MissingAttribute, - string.Format( - CultureInfo.InvariantCulture, - DicomCoreResource.MissingRequiredTag, - tag)); - } - - if (isNonZero) - { - if (dataset.Contains(tag) && dataset.GetValueCount(tag) < 1) - { - throw new DatasetValidationException( - FailureReasonCodes.MissingAttributeValue, - string.Format( - CultureInfo.InvariantCulture, - DicomCoreResource.MissingRequiredValue, - tag)); - } - - if (StringVRs.Contains(tag.GetDefaultVR().Code) - && dataset.TryGetString(tag, out string newStringValue) - && string.IsNullOrWhiteSpace(newStringValue)) - { - throw new DatasetValidationException( - FailureReasonCodes.ValidationFailure, - string.Format( - CultureInfo.InvariantCulture, - DicomCoreResource.AttributeMustNotBeEmpty, - tag)); - } - } - } - - private static void ValidateEmptyValue(this DicomDataset dataset, DicomTag tag) - { - EnsureArg.IsNotNull(dataset, nameof(dataset)); - - if (dataset.GetValueCount(tag) > 0) - { - throw new DatasetValidationException( - FailureReasonCodes.ValidationFailure, - string.Format( - CultureInfo.InvariantCulture, - DicomCoreResource.AttributeMustBeEmpty, - tag)); - } - } - - private static void ValidateNotPresent(this DicomDataset dataset, DicomTag tag) - { - EnsureArg.IsNotNull(dataset, nameof(dataset)); - - if (dataset.Contains(tag)) - { - throw new DatasetValidationException( - FailureReasonCodes.ValidationFailure, - string.Format( - CultureInfo.InvariantCulture, - DicomCoreResource.AttributeNotAllowed, - tag)); - } - } - - /// - /// Try to update dicom dataset after updating existing dataset with tag value present in newDataset. - /// - /// - /// Update for a tag happens regardless of whether the tag already had value in the existing dataset or not. - /// - /// Existing Dataset. - /// New Dataset. - /// Tag to be updated. - /// Dataset after updating based on values in . - public static void AddOrUpdate(this DicomDataset existingDataset, DicomDataset newDataset, DicomTag tag, out DicomDataset updatedDataset) - { - EnsureArg.IsNotNull(existingDataset, nameof(existingDataset)); - EnsureArg.IsNotNull(newDataset, nameof(newDataset)); - EnsureArg.IsNotNull(tag, nameof(tag)); - - updatedDataset = existingDataset; - - switch (tag.GetDefaultVR().Code) - { - case var code when DecimalVRs.Contains(code): - updatedDataset = existingDataset.AddOrUpdate(tag, newDataset.GetFirstValueOrDefault(tag)); - break; - case var code when DoubleVRs.Contains(code): - updatedDataset = existingDataset.AddOrUpdate(tag, newDataset.GetFirstValueOrDefault(tag)); - break; - case var code when FloatVRs.Contains(code): - updatedDataset = existingDataset.AddOrUpdate(tag, newDataset.GetFirstValueOrDefault(tag)); - break; - case var code when IntVRs.Contains(code): - updatedDataset = existingDataset.AddOrUpdate(tag, newDataset.GetFirstValueOrDefault(tag)); - break; - case var code when LongVRs.Contains(code): - updatedDataset = existingDataset.AddOrUpdate(tag, newDataset.GetFirstValueOrDefault(tag)); - break; - case var code when ShortVRs.Contains(code): - updatedDataset = existingDataset.AddOrUpdate(tag, newDataset.GetFirstValueOrDefault(tag)); - break; - case var code when StringVRs.Contains(code): - updatedDataset = existingDataset.AddOrUpdate(tag, newDataset.GetString(tag)); - break; - case var code when UIntVRs.Contains(code): - updatedDataset = existingDataset.AddOrUpdate(tag, newDataset.GetFirstValueOrDefault(tag)); - break; - case var code when ULongVRs.Contains(code): - updatedDataset = existingDataset.AddOrUpdate(tag, newDataset.GetFirstValueOrDefault(tag)); - break; - case var code when UShortVRs.Contains(code): - updatedDataset = existingDataset.AddOrUpdate(tag, newDataset.GetFirstValueOrDefault(tag)); - break; - case var code when ByteArrayVRs.Contains(code): - updatedDataset = existingDataset.AddOrUpdate(tag, newDataset.GetValues(tag)); - break; - case "SQ": - newDataset.CopyTo(updatedDataset, tag); - break; - // Other VR Types - case "OD": - updatedDataset = existingDataset.AddOrUpdate(tag, newDataset.GetValues(tag)); - break; - case "OF": - updatedDataset = existingDataset.AddOrUpdate(tag, newDataset.GetValues(tag)); - break; - case "OL": - updatedDataset = existingDataset.AddOrUpdate(tag, newDataset.GetValues(tag)); - break; - case "OW": - updatedDataset = existingDataset.AddOrUpdate(tag, newDataset.GetValues(tag)); - break; - case "OV": - updatedDataset = existingDataset.AddOrUpdate(tag, newDataset.GetValues(tag)); - break; - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Extensions/DicomElementExtensions.cs b/src/Microsoft.Health.Dicom.Core/Extensions/DicomElementExtensions.cs deleted file mode 100644 index 68b81a3deb..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Extensions/DicomElementExtensions.cs +++ /dev/null @@ -1,37 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using FellowOakDicom; - -namespace Microsoft.Health.Dicom.Core.Extensions; - -/// -/// Extension methods for . -/// -public static class DicomElementExtensions -{ - /// - /// Get first value of DicomElement if exists, otherwise return default() - /// - /// Value Type. - /// The dicom element. - /// The value. - public static T GetFirstValueOrDefault(this DicomElement dicomElement) - { - EnsureArg.IsNotNull(dicomElement, nameof(dicomElement)); - if (dicomElement.Count == 0) - { - return default(T); - } - - if (dicomElement.Count == 1) - { - return dicomElement.Get(); - } - return dicomElement.Get(0); - } - -} diff --git a/src/Microsoft.Health.Dicom.Core/Extensions/DicomMediatorExtensions.cs b/src/Microsoft.Health.Dicom.Core/Extensions/DicomMediatorExtensions.cs deleted file mode 100644 index 6ea934f2f1..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Extensions/DicomMediatorExtensions.cs +++ /dev/null @@ -1,330 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using FellowOakDicom; -using MediatR; -using Microsoft.Health.Dicom.Core.Features.ChangeFeed; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Features.Query; -using Microsoft.Health.Dicom.Core.Messages.ChangeFeed; -using Microsoft.Health.Dicom.Core.Messages.Delete; -using Microsoft.Health.Dicom.Core.Messages.Export; -using Microsoft.Health.Dicom.Core.Messages.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Messages.Operations; -using Microsoft.Health.Dicom.Core.Messages.Partitioning; -using Microsoft.Health.Dicom.Core.Messages.Query; -using Microsoft.Health.Dicom.Core.Messages.Retrieve; -using Microsoft.Health.Dicom.Core.Messages.Store; -using Microsoft.Health.Dicom.Core.Messages.Update; -using Microsoft.Health.Dicom.Core.Messages.Workitem; -using Microsoft.Health.Dicom.Core.Models; -using Microsoft.Health.Dicom.Core.Models.Export; -using Microsoft.Health.Dicom.Core.Models.Update; -using ResourceType = Microsoft.Health.Dicom.Core.Messages.ResourceType; - -namespace Microsoft.Health.Dicom.Core.Extensions; - -public static class DicomMediatorExtensions -{ - public static Task StoreDicomResourcesAsync( - this IMediator mediator, Stream requestBody, string requestContentType, string studyInstanceUid, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(mediator, nameof(mediator)); - return mediator.Send(new StoreRequest(requestBody, requestContentType, studyInstanceUid), cancellationToken); - } - - public static Task RetrieveDicomStudyAsync( - this IMediator mediator, string studyInstanceUid, IReadOnlyCollection acceptHeaders, bool isOriginalVersionRequested, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(mediator, nameof(mediator)); - return mediator.Send( - new RetrieveResourceRequest(studyInstanceUid, acceptHeaders, isOriginalVersionRequested), - cancellationToken); - } - - public static Task RetrieveDicomStudyMetadataAsync( - this IMediator mediator, string studyInstanceUid, string ifNoneMatch, bool isOriginalVersionRequested, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(mediator, nameof(mediator)); - return mediator.Send(new RetrieveMetadataRequest(studyInstanceUid, ifNoneMatch, isOriginalVersionRequested), cancellationToken); - } - - public static Task RetrieveDicomSeriesAsync( - this IMediator mediator, string studyInstanceUid, string seriesInstanceUid, IReadOnlyCollection acceptHeaders, bool isOriginalVersionRequested, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(mediator, nameof(mediator)); - return mediator.Send( - new RetrieveResourceRequest(studyInstanceUid, seriesInstanceUid, acceptHeaders, isOriginalVersionRequested), - cancellationToken); - } - - public static Task RetrieveDicomSeriesMetadataAsync( - this IMediator mediator, string studyInstanceUid, string seriesInstanceUid, string ifNoneMatch, bool isOriginalVersionRequested, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(mediator, nameof(mediator)); - return mediator.Send(new RetrieveMetadataRequest(studyInstanceUid, seriesInstanceUid, ifNoneMatch, isOriginalVersionRequested), cancellationToken); - } - - public static Task RetrieveDicomInstanceAsync( - this IMediator mediator, - string studyInstanceUid, - string seriesInstanceUid, - string sopInstanceUid, - IReadOnlyCollection acceptHeaders, - bool isOriginalVersionRequested, - CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(mediator, nameof(mediator)); - return mediator.Send( - new RetrieveResourceRequest(studyInstanceUid, seriesInstanceUid, sopInstanceUid, acceptHeaders, isOriginalVersionRequested), - cancellationToken); - } - - public static Task RetrieveRenderedDicomInstanceAsync( - this IMediator mediator, string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid, ResourceType resourceType, IReadOnlyCollection acceptHeaders, int quality, CancellationToken cancellationToken, int frameNumber = 1) - { - EnsureArg.IsNotNull(mediator, nameof(mediator)); - return mediator.Send( - new RetrieveRenderedRequest(studyInstanceUid, seriesInstanceUid, sopInstanceUid, resourceType, frameNumber, quality, acceptHeaders), - cancellationToken); - } - - public static Task RetrieveDicomInstanceMetadataAsync( - this IMediator mediator, string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid, string ifNoneMatch, bool isOriginalVersionRequested, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(mediator, nameof(mediator)); - return mediator.Send(new RetrieveMetadataRequest(studyInstanceUid, seriesInstanceUid, sopInstanceUid, ifNoneMatch, isOriginalVersionRequested), cancellationToken); - } - - public static Task RetrieveDicomFramesAsync( - this IMediator mediator, string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid, int[] frames, IReadOnlyCollection acceptHeaders, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(mediator, nameof(mediator)); - return mediator.Send( - new RetrieveResourceRequest(studyInstanceUid, seriesInstanceUid, sopInstanceUid, frames, acceptHeaders), - cancellationToken); - } - - public static Task DeleteDicomStudyAsync( - this IMediator mediator, string studyInstanceUid, CancellationToken cancellationToken = default) - { - EnsureArg.IsNotNull(mediator, nameof(mediator)); - return mediator.Send(new DeleteResourcesRequest(studyInstanceUid), cancellationToken); - } - - public static Task DeleteDicomSeriesAsync( - this IMediator mediator, string studyInstanceUid, string seriesInstanceUid, CancellationToken cancellationToken = default) - { - EnsureArg.IsNotNull(mediator, nameof(mediator)); - return mediator.Send(new DeleteResourcesRequest(studyInstanceUid, seriesInstanceUid), cancellationToken); - } - - public static Task DeleteDicomInstanceAsync( - this IMediator mediator, string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid, CancellationToken cancellationToken = default) - { - EnsureArg.IsNotNull(mediator, nameof(mediator)); - return mediator.Send(new DeleteResourcesRequest(studyInstanceUid, seriesInstanceUid, sopInstanceUid), cancellationToken); - } - - public static Task QueryDicomResourcesAsync( - this IMediator mediator, - QueryParameters parameters, - CancellationToken cancellationToken = default) - { - EnsureArg.IsNotNull(mediator, nameof(mediator)); - EnsureArg.IsNotNull(parameters, nameof(parameters)); - return mediator.Send(new QueryResourceRequest(parameters), cancellationToken); - } - - public static Task GetChangeFeed( - this IMediator mediator, - TimeRange range, - long offset, - int limit, - ChangeFeedOrder order, - bool includeMetadata, - CancellationToken cancellationToken = default) - { - EnsureArg.IsNotNull(mediator, nameof(mediator)); - return mediator.Send(new ChangeFeedRequest(range, offset, limit, order, includeMetadata), cancellationToken); - } - - public static Task GetChangeFeedLatest( - this IMediator mediator, - ChangeFeedOrder order, - bool includeMetadata, - CancellationToken cancellationToken = default) - { - EnsureArg.IsNotNull(mediator, nameof(mediator)); - return mediator.Send(new ChangeFeedLatestRequest(order, includeMetadata), cancellationToken); - } - - public static Task AddExtendedQueryTagsAsync( - this IMediator mediator, IEnumerable extendedQueryTags, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(mediator, nameof(mediator)); - return mediator.Send(new AddExtendedQueryTagRequest(extendedQueryTags), cancellationToken); - } - - public static Task DeleteExtendedQueryTagAsync( - this IMediator mediator, string tagPath, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(mediator, nameof(mediator)); - return mediator.Send(new DeleteExtendedQueryTagRequest(tagPath), cancellationToken); - } - - public static Task GetExtendedQueryTagsAsync( - this IMediator mediator, int limit, long offset, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(mediator, nameof(mediator)); - return mediator.Send(new GetExtendedQueryTagsRequest(limit, offset), cancellationToken); - } - - public static Task GetExtendedQueryTagAsync( - this IMediator mediator, string extendedQueryTagPath, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(mediator, nameof(mediator)); - return mediator.Send(new GetExtendedQueryTagRequest(extendedQueryTagPath), cancellationToken); - } - - public static Task GetExtendedQueryTagErrorsAsync( - this IMediator mediator, string extendedQueryTagPath, int limit, long offset, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(mediator, nameof(mediator)); - return mediator.Send(new GetExtendedQueryTagErrorsRequest(extendedQueryTagPath, limit, offset), cancellationToken); - } - - public static Task UpdateExtendedQueryTagAsync( - this IMediator mediator, string tagPath, UpdateExtendedQueryTagEntry newValue, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(mediator, nameof(mediator)); - return mediator.Send(new UpdateExtendedQueryTagRequest(tagPath, newValue), cancellationToken); - } - - public static Task ExportAsync( - this IMediator mediator, - ExportSpecification spec, - CancellationToken cancellationToken = default) - { - EnsureArg.IsNotNull(mediator, nameof(mediator)); - return mediator.Send(new ExportRequest(spec), cancellationToken); - } - - public static Task GetOperationStateAsync( - this IMediator mediator, - Guid operationId, - CancellationToken cancellationToken = default) - { - EnsureArg.IsNotNull(mediator, nameof(mediator)); - return mediator.Send(new OperationStateRequest(operationId), cancellationToken); - } - - public static Task GetPartitionAsync( - this IMediator mediator, - string partitionName, - CancellationToken cancellationToken = default) - { - EnsureArg.IsNotNull(mediator, nameof(mediator)); - return mediator.Send(new GetPartitionRequest(partitionName), cancellationToken); - } - - public static Task GetOrAddPartitionAsync( - this IMediator mediator, - string partitionName, - CancellationToken cancellationToken = default) - { - EnsureArg.IsNotNull(mediator, nameof(mediator)); - return mediator.Send(new GetOrAddPartitionRequest(partitionName), cancellationToken); - } - - public static Task GetPartitionsAsync( - this IMediator mediator, - CancellationToken cancellationToken = default) - { - EnsureArg.IsNotNull(mediator, nameof(mediator)); - return mediator.Send(new GetPartitionsRequest(), cancellationToken); - } - - public static Task AddWorkitemAsync( - this IMediator mediator, DicomDataset dicomDataSet, string requestContentType, string workitemInstanceUid, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(mediator, nameof(mediator)); - - return mediator.Send(new AddWorkitemRequest(dicomDataSet, requestContentType, workitemInstanceUid), cancellationToken); - } - - public static Task CancelWorkitemAsync( - this IMediator mediator, DicomDataset dicomDataSet, string requestContentType, string workitemUid, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(mediator, nameof(mediator)); - - return mediator.Send(new CancelWorkitemRequest(dicomDataSet, requestContentType, workitemUid), cancellationToken); - } - - public static Task QueryWorkitemsAsync( - this IMediator mediator, - BaseQueryParameters parameters, - CancellationToken cancellationToken = default) - { - EnsureArg.IsNotNull(mediator, nameof(mediator)); - EnsureArg.IsNotNull(parameters, nameof(parameters)); - return mediator.Send(new QueryWorkitemResourceRequest(parameters), cancellationToken); - } - - public static Task ChangeWorkitemStateAsync( - this IMediator mediator, - DicomDataset dicomDataSet, - string requestContentType, - string workitemUid, - CancellationToken cancellationToken = default) - { - EnsureArg.IsNotNull(mediator, nameof(mediator)); - EnsureArg.IsNotEmptyOrWhiteSpace(workitemUid, nameof(workitemUid)); - - return mediator.Send( - new ChangeWorkitemStateRequest(dicomDataSet, requestContentType, workitemUid), cancellationToken); - } - - public static Task RetrieveWorkitemAsync( - this IMediator mediator, - string workitemInstanceUid, - CancellationToken cancellationToken = default) - { - EnsureArg.IsNotNull(mediator, nameof(mediator)); - EnsureArg.IsNotEmptyOrWhiteSpace(workitemInstanceUid, nameof(workitemInstanceUid)); - - return mediator.Send(new RetrieveWorkitemRequest(workitemInstanceUid), cancellationToken); - } - - public static Task UpdateWorkitemAsync( - this IMediator mediator, - DicomDataset dicomDataset, - string requestContentType, - string workitemInstanceUid, - string transactionUid = default, - CancellationToken cancellationToken = default) - { - EnsureArg.IsNotNull(mediator, nameof(mediator)); - EnsureArg.IsNotEmptyOrWhiteSpace(workitemInstanceUid, nameof(workitemInstanceUid)); - - // Not validating transaction Uid as it can be null if the procedure step state is in SCHEDULED state. - return mediator.Send(new UpdateWorkitemRequest(dicomDataset, requestContentType, workitemInstanceUid, transactionUid), cancellationToken); - } - - public static Task UpdateInstanceAsync( - this IMediator mediator, - UpdateSpecification updateSpecification, - CancellationToken cancellationToken = default) - { - EnsureArg.IsNotNull(mediator, nameof(mediator)); - return mediator.Send(new UpdateInstanceRequest(updateSpecification), cancellationToken); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Extensions/DicomRequestContextExtensions.cs b/src/Microsoft.Health.Dicom.Core/Extensions/DicomRequestContextExtensions.cs deleted file mode 100644 index 565c1d6b33..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Extensions/DicomRequestContextExtensions.cs +++ /dev/null @@ -1,34 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using Microsoft.Health.Dicom.Core.Features.Context; -using Microsoft.Health.Dicom.Core.Features.Partitioning; - -namespace Microsoft.Health.Dicom.Core.Extensions; - -public static class DicomRequestContextExtensions -{ - public static int GetPartitionKey(this IDicomRequestContext dicomRequestContext) - { - EnsureArg.IsNotNull(dicomRequestContext, nameof(dicomRequestContext)); - - return dicomRequestContext.DataPartition.Key; - } - - public static string GetPartitionName(this IDicomRequestContext dicomRequestContext) - { - EnsureArg.IsNotNull(dicomRequestContext, nameof(dicomRequestContext)); - - return dicomRequestContext.DataPartition.Name; - } - - public static Partition GetPartition(this IDicomRequestContext dicomRequestContext) - { - EnsureArg.IsNotNull(dicomRequestContext, nameof(dicomRequestContext)); - - return dicomRequestContext.DataPartition; - } -} \ No newline at end of file diff --git a/src/Microsoft.Health.Dicom.Core/Extensions/DicomTagExtensions.cs b/src/Microsoft.Health.Dicom.Core/Extensions/DicomTagExtensions.cs deleted file mode 100644 index 8ebfa1d326..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Extensions/DicomTagExtensions.cs +++ /dev/null @@ -1,57 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Globalization; -using EnsureThat; -using FellowOakDicom; - -namespace Microsoft.Health.Dicom.Core.Extensions; - -/// -/// Extension methods for . -/// -public static class DicomTagExtensions -{ - /// - /// Get path of given Dicom Tag. - /// e.g:Path of Dicom tag (0008,0070) is 00080070 - /// - /// The dicom tag - /// The path. - public static string GetPath(this DicomTag dicomTag) - { - EnsureArg.IsNotNull(dicomTag, nameof(dicomTag)); - return dicomTag.Group.ToString("X4", CultureInfo.InvariantCulture) + dicomTag.Element.ToString("X4", CultureInfo.InvariantCulture); - } - - /// - /// Get default VR for dicom tag. - /// - /// If the dicom tag is unknown tag or private tag except PrivateCreator tag (gggg,0010-00FF) , is returned. - /// The dicm tag - /// The default VR. - public static DicomVR GetDefaultVR(this DicomTag dicomTag) - { - EnsureArg.IsNotNull(dicomTag, nameof(dicomTag)); - if (dicomTag.DictionaryEntry == DicomDictionary.UnknownTag) - { - // this tag is private or invalid tag. - return null; - } - - return dicomTag.DictionaryEntry.ValueRepresentations.Length > 0 ? dicomTag.DictionaryEntry.ValueRepresentations[0] : null; - } - - /// - /// Get friendly name of dicom tag. - /// - /// The dicom tag. - /// The friendly name. - public static string GetFriendlyName(this DicomTag dicomTag) - { - EnsureArg.IsNotNull(dicomTag, nameof(dicomTag)); - return dicomTag.IsPrivate ? dicomTag.GetPath() : dicomTag.DictionaryEntry.Keyword; - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Extensions/DicomTelemetryClientExtensions.cs b/src/Microsoft.Health.Dicom.Core/Extensions/DicomTelemetryClientExtensions.cs deleted file mode 100644 index 5fc1bb5d78..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Extensions/DicomTelemetryClientExtensions.cs +++ /dev/null @@ -1,36 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using Microsoft.Health.Dicom.Core.Features.Telemetry; - -namespace Microsoft.Health.Dicom.Core.Extensions; - -internal static class DicomTelemetryClientExtensions -{ - public static void TrackInstanceCount(this IDicomTelemetryClient telemetryClient, int count) - { - EnsureArg.IsNotNull(telemetryClient, nameof(telemetryClient)); - telemetryClient.TrackMetric("InstanceCount", count); - } - - public static void TrackTotalInstanceBytes(this IDicomTelemetryClient telemetryClient, long bytes) - { - EnsureArg.IsNotNull(telemetryClient, nameof(telemetryClient)); - telemetryClient.TrackMetric("TotalInstanceBytes", bytes); - } - - public static void TrackMinInstanceBytes(this IDicomTelemetryClient telemetryClient, long bytes) - { - EnsureArg.IsNotNull(telemetryClient, nameof(telemetryClient)); - telemetryClient.TrackMetric("MinInstanceBytes", bytes); - } - - public static void TrackMaxInstanceBytes(this IDicomTelemetryClient telemetryClient, long bytes) - { - EnsureArg.IsNotNull(telemetryClient, nameof(telemetryClient)); - telemetryClient.TrackMetric("MaxInstanceBytes", bytes); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Extensions/ExtendedQueryTagEntryExtensions.cs b/src/Microsoft.Health.Dicom.Core/Extensions/ExtendedQueryTagEntryExtensions.cs deleted file mode 100644 index 630e1ab068..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Extensions/ExtendedQueryTagEntryExtensions.cs +++ /dev/null @@ -1,59 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Globalization; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -namespace Microsoft.Health.Dicom.Core.Extensions; - -/// -/// Extension methods for . -/// -internal static class ExtendedQueryTagEntryExtensions -{ - /// - /// Normalize extended query tag entry before saving to ExtendedQueryTagStore. - /// - /// The extended query tag entry. - /// Normalize extended query tag entry. - public static AddExtendedQueryTagEntry Normalize(this AddExtendedQueryTagEntry extendedQueryTagEntry) - { - DicomTagParser dicomTagParser = new DicomTagParser(); - DicomTag[] tags; - if (!dicomTagParser.TryParse(extendedQueryTagEntry.Path, out tags)) - { - // not a valid dicom tag path - throw new ExtendedQueryTagEntryValidationException( - string.Format(CultureInfo.InvariantCulture, DicomCoreResource.InvalidExtendedQueryTag, extendedQueryTagEntry)); - } - - DicomTag tag = tags[0]; - string path = tag.GetPath(); - string vr = extendedQueryTagEntry.VR; - string privateCreator = string.IsNullOrWhiteSpace(extendedQueryTagEntry.PrivateCreator) ? null : extendedQueryTagEntry.PrivateCreator; - - // when VR is not specified for known tags - if (tag.DictionaryEntry != DicomDictionary.UnknownTag) - { - if (string.IsNullOrWhiteSpace(vr)) - { - vr = tag.GetDefaultVR()?.Code; - } - } - - vr = vr?.ToUpperInvariant(); - - return new AddExtendedQueryTagEntry() - { - Path = path, - VR = vr, - PrivateCreator = privateCreator, - Level = extendedQueryTagEntry.Level, - }; - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Extensions/JsonSerializerOptionsExtensions.cs b/src/Microsoft.Health.Dicom.Core/Extensions/JsonSerializerOptionsExtensions.cs deleted file mode 100644 index 271754e1f8..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Extensions/JsonSerializerOptionsExtensions.cs +++ /dev/null @@ -1,50 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Text.Json; -using System.Text.Json.Serialization; -using EnsureThat; -using Microsoft.Health.Dicom.Core.Serialization; -using Microsoft.Health.FellowOakDicom.Serialization; - -namespace Microsoft.Health.Dicom.Core.Extensions; - -/// -/// Provides methods configuring . -/// -public static class JsonSerializerOptionsExtensions -{ - /// - /// Configures an instance of for usage within - /// the DICOM server and related services. - /// - /// A set of existing options. - /// is . - public static void ConfigureDefaultDicomSettings(this JsonSerializerOptions options) - { - EnsureArg.IsNotNull(options, nameof(options)); - - options.Converters.Clear(); - options.Converters.Add(new DicomIdentifierJsonConverter()); - options.Converters.Add(new DicomJsonConverter(writeTagsAsKeywords: false, autoValidate: false, numberSerializationMode: NumberSerializationMode.PreferablyAsNumber)); - options.Converters.Add(new ExportDataOptionsJsonConverter()); - options.Converters.Add(new StrictStringEnumConverter(JsonNamingPolicy.CamelCase)); - - options.AllowTrailingCommas = true; - options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; - options.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase; - options.Encoder = null; - options.IgnoreReadOnlyFields = false; - options.IgnoreReadOnlyProperties = false; - options.IncludeFields = false; - options.MaxDepth = 0; // 0 indicates the max depth of 64 - options.NumberHandling = JsonNumberHandling.Strict; - options.PropertyNameCaseInsensitive = true; - options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; - options.ReadCommentHandling = JsonCommentHandling.Skip; - options.WriteIndented = false; - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Extensions/JsonSerializerSettingsExtensions.cs b/src/Microsoft.Health.Dicom.Core/Extensions/JsonSerializerSettingsExtensions.cs deleted file mode 100644 index 26e3db9830..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Extensions/JsonSerializerSettingsExtensions.cs +++ /dev/null @@ -1,45 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using EnsureThat; -using Microsoft.Health.Dicom.Core.Serialization.Newtonsoft; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Newtonsoft.Json.Serialization; - -namespace Microsoft.Health.Dicom.Core.Extensions; - -/// -/// Provides methods configuring . -/// -public static class JsonSerializerSettingsExtensions -{ - /// - /// Configures an instance of for usage within - /// the DICOM server and related services. - /// - /// A set of existing options. - /// is . - public static void ConfigureDefaultDicomSettings(this JsonSerializerSettings settings) - { - EnsureArg.IsNotNull(settings, nameof(settings)); - - NamingStrategy camelCase = new CamelCaseNamingStrategy(); - - settings.Converters.Clear(); - settings.Converters.Add(new DicomIdentifierJsonConverter()); - settings.Converters.Add(new ExportDestinationOptionsJsonConverter(camelCase)); - settings.Converters.Add(new ExportSourceOptionsJsonConverter(camelCase)); - settings.Converters.Add(new StringEnumConverter(camelCase)); - - settings.ContractResolver = new DefaultContractResolver { NamingStrategy = camelCase }; - settings.DateFormatHandling = DateFormatHandling.IsoDateFormat; - settings.DateParseHandling = DateParseHandling.None; - settings.DateTimeZoneHandling = DateTimeZoneHandling.Utc; - settings.NullValueHandling = NullValueHandling.Ignore; - settings.TypeNameHandling = TypeNameHandling.None; - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Extensions/ValidationErrorCodeExtensions.cs b/src/Microsoft.Health.Dicom.Core/Extensions/ValidationErrorCodeExtensions.cs deleted file mode 100644 index 0baa14f6e7..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Extensions/ValidationErrorCodeExtensions.cs +++ /dev/null @@ -1,57 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics; -using Microsoft.Health.Dicom.Core.Features.Validation; - -namespace Microsoft.Health.Dicom.Core.Extensions; - -public static class ValidationErrorCodeExtensions -{ - private static readonly ImmutableDictionary MessageMap = ImmutableDictionary.CreateRange( - new KeyValuePair[] - { - KeyValuePair.Create(ValidationErrorCode.None, string.Empty), - - KeyValuePair.Create(ValidationErrorCode.MultipleValues, DicomCoreResource.ErrorMessageMultiValues), - KeyValuePair.Create(ValidationErrorCode.ExceedMaxLength, DicomCoreResource.SimpleErrorMessageExceedMaxLength), - KeyValuePair.Create(ValidationErrorCode.UnexpectedLength, DicomCoreResource.SimpleErrorMessageUnexpectedLength), - KeyValuePair.Create(ValidationErrorCode.InvalidCharacters, DicomCoreResource.ErrorMessageInvalidCharacters), - KeyValuePair.Create(ValidationErrorCode.UnexpectedVR, DicomCoreResource.SimpleErrorMessageUnexpectedVR), - KeyValuePair.Create(ValidationErrorCode.ImplicitVRNotAllowed, DicomCoreResource.ImplicitVRNotAllowed), - - KeyValuePair.Create(ValidationErrorCode.PersonNameExceedMaxGroups, DicomCoreResource.ErrorMessagePersonNameExceedMaxComponents), - KeyValuePair.Create(ValidationErrorCode.PersonNameGroupExceedMaxLength, DicomCoreResource.ErrorMessagePersonNameGroupExceedMaxLength), - KeyValuePair.Create(ValidationErrorCode.PersonNameExceedMaxComponents, DicomCoreResource.ErrorMessagePersonNameExceedMaxComponents), - - KeyValuePair.Create(ValidationErrorCode.DateIsInvalid, DicomCoreResource.ErrorMessageDateIsInvalid), - KeyValuePair.Create(ValidationErrorCode.UidIsInvalid, DicomCoreResource.ErrorMessageUidIsInvalid), - KeyValuePair.Create(ValidationErrorCode.DateTimeIsInvalid, DicomCoreResource.ErrorMessageDateTimeIsInvalid), - KeyValuePair.Create(ValidationErrorCode.TimeIsInvalid, DicomCoreResource.ErrorMessageTimeIsInvalid), - KeyValuePair.Create(ValidationErrorCode.IntegerStringIsInvalid, DicomCoreResource.ErrorMessageIntegerStringIsInvalid), - KeyValuePair.Create(ValidationErrorCode.SequenceDisallowed, DicomCoreResource.SequentialDicomTagsNotSupported), - KeyValuePair.Create(ValidationErrorCode.NestedSequence, DicomCoreResource.NestedSequencesNotSupported), - }); - - /// - /// Get error message for error code. - /// - /// The error code - /// The message - public static string GetMessage(this ValidationErrorCode errorCode) - { - if (MessageMap.TryGetValue(errorCode, out string message)) - { - return message; - } - else - { - Debug.Fail($"Missing message for error code {errorCode}"); - return string.Empty; - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Audit/AuditEventSubType.cs b/src/Microsoft.Health.Dicom.Core/Features/Audit/AuditEventSubType.cs deleted file mode 100644 index 1ca290e86b..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Audit/AuditEventSubType.cs +++ /dev/null @@ -1,64 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Features.Audit; - -/// -/// Value set defined at http://dicom.nema.org/medical/dicom/current/output/html/part15.html#sect_A.5.1 -/// -public static class AuditEventSubType -{ - public const string System = "http://dicom.nema.org/medical/dicom/current/output/html/part15.html#sect_A.5.1"; - - public const string Partition = "partition"; - - public const string ChangeFeed = "change-feed"; - - public const string Delete = "delete"; - - public const string Query = "query"; - - public const string Retrieve = "retrieve"; - - public const string RetrieveMetadata = "retrieve-metadata"; - - public const string RetrieveRendered = "retrieve-rendered"; - - public const string Store = "store"; - - public const string Export = "export"; - - public const string Operation = "operation"; - - public const string AddWorkitem = "add-workitem"; - - public const string CancelWorkitem = "cancel-workitem"; - - public const string QueryWorkitem = "query-workitem"; - - public const string RetrieveWorkitem = "retrieve-workitem"; - - public const string ChangeStateWorkitem = "change-state-workitem"; - - public const string UpdateWorkitem = "update-workitem"; - - public const string AddExtendedQueryTag = "add-extended-query-tag"; - - public const string RemoveExtendedQueryTag = "remove-extended-query-tag"; - - public const string GetAllExtendedQueryTags = "get-all-extended-query-tag"; - - public const string GetExtendedQueryTag = "get-extended-query-tag"; - - public const string GetExtendedQueryTagErrors = "get-extended-query-tag-errors"; - - public const string UpdateExtendedQueryTag = "update-extended-query-tag"; - - public const string BulkImportStore = "bulk-import-store"; - - public const string UpdateStudy = "update-study"; - - public const string UpdateStudyOperation = "update-study-operation"; -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Audit/AuditEventType.cs b/src/Microsoft.Health.Dicom.Core/Features/Audit/AuditEventType.cs deleted file mode 100644 index 107adc9f09..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Audit/AuditEventType.cs +++ /dev/null @@ -1,16 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Features.Audit; - -/// -/// Value set defined at http://dicom.nema.org/medical/dicom/current/output/html/part15.html#sect_A.5 -/// -public static class AuditEventType -{ - public const string System = "http://dicom.nema.org/medical/dicom/current/output/html/part15.html#sect_A.5"; - - public const string RestFulOperationCode = "rest"; -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Audit/AuditLogger.cs b/src/Microsoft.Health.Dicom.Core/Features/Audit/AuditLogger.cs deleted file mode 100644 index a4f00bd81f..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Audit/AuditLogger.cs +++ /dev/null @@ -1,91 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using EnsureThat; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Health.Core.Features.Audit; -using Microsoft.Health.Dicom.Core.Configs; - -namespace Microsoft.Health.Dicom.Core.Features.Audit; - -/// -/// Provides mechanism to log the audit event using default logger. -/// -public class AuditLogger : IAuditLogger -{ - private const string AuditEventType = "AuditEvent"; - - private static readonly string AuditMessageFormat = - "ActionType: {ActionType}" + Environment.NewLine + - "EventType: {EventType}" + Environment.NewLine + - "Audience: {Audience}" + Environment.NewLine + - "Authority: {Authority}" + Environment.NewLine + - "RequestUri: {ResourceUri}" + Environment.NewLine + - "Action: {Action}" + Environment.NewLine + - "StatusCode: {StatusCode}" + Environment.NewLine + - "CorrelationId: {CorrelationId}" + Environment.NewLine + - "Claims: {Claims}"; - - private readonly SecurityConfiguration _securityConfiguration; - private readonly ILogger _logger; - - public AuditLogger( - IOptions securityConfiguration, - ILogger logger) - { - EnsureArg.IsNotNull(securityConfiguration?.Value, nameof(securityConfiguration)); - EnsureArg.IsNotNull(logger, nameof(logger)); - - _securityConfiguration = securityConfiguration.Value; - _logger = logger; - } - - /// - public void LogAudit( - AuditAction auditAction, - string operation, - Uri requestUri, - HttpStatusCode? statusCode, - string correlationId, - string callerIpAddress, - IReadOnlyCollection> callerClaims, - IReadOnlyDictionary customHeaders = null) - { - string claimsInString = null; - string customerHeadersInString = null; - - if (callerClaims != null) - { - claimsInString = string.Join(";", callerClaims.Select(claim => $"{claim.Key}={claim.Value}")); - } - - if (customHeaders != null) - { - customerHeadersInString = string.Join(";", customHeaders.Select(header => $"{header.Key}={header.Value}")); - } - -#pragma warning disable CA2254 - // AuditMessageFormat is not const and erroneously flags CA2254. - // While the template does indeed change per OS, it does not change the variables in use. - _logger.LogInformation( - AuditMessageFormat, - auditAction, - AuditEventType, - _securityConfiguration.Authentication?.Audience, - _securityConfiguration.Authentication?.Authority, - requestUri, - operation, - statusCode, - correlationId, - claimsInString, - customerHeadersInString); -#pragma warning restore CA2254 - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Audit/IAuditLogger.cs b/src/Microsoft.Health.Dicom.Core/Features/Audit/IAuditLogger.cs deleted file mode 100644 index 6cdc3a5464..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Audit/IAuditLogger.cs +++ /dev/null @@ -1,38 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Net; -using Microsoft.Health.Core.Features.Audit; - -namespace Microsoft.Health.Dicom.Core.Features.Audit; - -/// -/// Provides mechanism to log audit event. -/// -public interface IAuditLogger -{ - /// - /// Logs an audit event. - /// - /// The action to audit. - /// The Dicom operation to audit. - /// The request URI. - /// The response status code (if any). - /// The correlation ID. - /// The caller IP address. - /// The claims of the caller. - /// Headers added by the caller with data to be added to the audit logs. - void LogAudit( - AuditAction auditAction, - string operation, - Uri requestUri, - HttpStatusCode? statusCode, - string correlationId, - string callerIpAddress, - IReadOnlyCollection> callerClaims, - IReadOnlyDictionary customHeaders = null); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/ChangeFeed/ChangeFeedAction.cs b/src/Microsoft.Health.Dicom.Core/Features/ChangeFeed/ChangeFeedAction.cs deleted file mode 100644 index 2e7460fd3b..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/ChangeFeed/ChangeFeedAction.cs +++ /dev/null @@ -1,13 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Features.ChangeFeed; - -public enum ChangeFeedAction -{ - Create = 0, - Delete = 1, - Update = 2, -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/ChangeFeed/ChangeFeedEntry.cs b/src/Microsoft.Health.Dicom.Core/Features/ChangeFeed/ChangeFeedEntry.cs deleted file mode 100644 index 01c69b7580..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/ChangeFeed/ChangeFeedEntry.cs +++ /dev/null @@ -1,92 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Text.Json.Serialization; -using EnsureThat; -using FellowOakDicom; - -namespace Microsoft.Health.Dicom.Core.Features.ChangeFeed; - -/// -/// Represents each change feed entry of a change has retrieved from the store -/// -public class ChangeFeedEntry -{ - public ChangeFeedEntry( - long sequence, - DateTimeOffset timestamp, - ChangeFeedAction action, - string studyInstanceUid, - string seriesInstanceUid, - string sopInstanceUid, - long originalVersion, - long? currentVersion, - ChangeFeedState state, - string partitionName = default, - DicomDataset metadata = null, - string filePath = null) - { - EnsureArg.IsNotNull(studyInstanceUid); - EnsureArg.IsNotNull(seriesInstanceUid); - EnsureArg.IsNotNull(sopInstanceUid); - - Sequence = sequence; - StudyInstanceUid = studyInstanceUid; - SeriesInstanceUid = seriesInstanceUid; - SopInstanceUid = sopInstanceUid; - Action = action; - Timestamp = timestamp; - State = state; - OriginalVersion = originalVersion; - CurrentVersion = currentVersion; - PartitionName = partitionName; - Metadata = metadata; - FilePath = filePath; - } - - public long Sequence { get; } - - public string PartitionName { get; } - - public string FilePath { get; } - - public string StudyInstanceUid { get; } - - public string SeriesInstanceUid { get; } - - public string SopInstanceUid { get; } - - [JsonConverter(typeof(JsonStringEnumConverter))] - public ChangeFeedAction Action { get; } - - public DateTimeOffset Timestamp { get; } - - [JsonConverter(typeof(JsonStringEnumConverter))] - public ChangeFeedState State { get; } - - [JsonIgnore] - public long OriginalVersion { get; } - - [JsonIgnore] - public long? CurrentVersion { get; } - - public DicomDataset Metadata { get; set; } - - /// - /// Control variable to determine whether or not the property should be included in a serialized view. - /// - [JsonIgnore] - public bool IncludeMetadata { get; set; } - - /// - /// Json.Net method for determining whether or not to serialize the property - /// - /// A boolean representing if the should be serialized. - public bool ShouldSerializeMetadata() - { - return IncludeMetadata; - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/ChangeFeed/ChangeFeedHandler.cs b/src/Microsoft.Health.Dicom.Core/Features/ChangeFeed/ChangeFeedHandler.cs deleted file mode 100644 index a6eb39fce8..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/ChangeFeed/ChangeFeedHandler.cs +++ /dev/null @@ -1,42 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using MediatR; -using Microsoft.Health.Core.Features.Security.Authorization; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Security; -using Microsoft.Health.Dicom.Core.Messages.ChangeFeed; - -namespace Microsoft.Health.Dicom.Core.Features.ChangeFeed; - -public class ChangeFeedHandler : BaseHandler, IRequestHandler -{ - private readonly IChangeFeedService _changeFeedService; - - public ChangeFeedHandler(IAuthorizationService authorizationService, IChangeFeedService changeFeedService) - : base(authorizationService) - { - _changeFeedService = EnsureArg.IsNotNull(changeFeedService, nameof(changeFeedService)); - } - - public async Task Handle(ChangeFeedRequest request, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(request, nameof(request)); - - if (await AuthorizationService.CheckAccess(DataActions.Read, cancellationToken) != DataActions.Read) - { - throw new UnauthorizedDicomActionException(DataActions.Read); - } - - IReadOnlyList changeFeedEntries = await _changeFeedService.GetChangeFeedAsync(request.Range, request.Offset, request.Limit, request.Order, request.IncludeMetadata, cancellationToken); - - return new ChangeFeedResponse(changeFeedEntries); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/ChangeFeed/ChangeFeedLatestHandler.cs b/src/Microsoft.Health.Dicom.Core/Features/ChangeFeed/ChangeFeedLatestHandler.cs deleted file mode 100644 index 73f58225b6..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/ChangeFeed/ChangeFeedLatestHandler.cs +++ /dev/null @@ -1,40 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using MediatR; -using Microsoft.Health.Core.Features.Security.Authorization; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Security; -using Microsoft.Health.Dicom.Core.Messages.ChangeFeed; - -namespace Microsoft.Health.Dicom.Core.Features.ChangeFeed; - -public class ChangeFeedLatestHandler : BaseHandler, IRequestHandler -{ - private readonly IChangeFeedService _changeFeedService; - - public ChangeFeedLatestHandler(IAuthorizationService authorizationService, IChangeFeedService changeFeedService) - : base(authorizationService) - { - _changeFeedService = EnsureArg.IsNotNull(changeFeedService, nameof(changeFeedService)); - } - - public async Task Handle(ChangeFeedLatestRequest request, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(request, nameof(request)); - - if (await AuthorizationService.CheckAccess(DataActions.Read, cancellationToken) != DataActions.Read) - { - throw new UnauthorizedDicomActionException(DataActions.Read); - } - - ChangeFeedEntry latestEntry = await _changeFeedService.GetChangeFeedLatestAsync(request.Order, request.IncludeMetadata, cancellationToken); - return new ChangeFeedLatestResponse(latestEntry); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/ChangeFeed/ChangeFeedOrder.cs b/src/Microsoft.Health.Dicom.Core/Features/ChangeFeed/ChangeFeedOrder.cs deleted file mode 100644 index d89440e0e8..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/ChangeFeed/ChangeFeedOrder.cs +++ /dev/null @@ -1,12 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Features.ChangeFeed; - -public enum ChangeFeedOrder -{ - Sequence, - Time, -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/ChangeFeed/ChangeFeedService.cs b/src/Microsoft.Health.Dicom.Core/Features/ChangeFeed/ChangeFeedService.cs deleted file mode 100644 index e45ae1567c..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/ChangeFeed/ChangeFeedService.cs +++ /dev/null @@ -1,79 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Models; - -namespace Microsoft.Health.Dicom.Core.Features.ChangeFeed; - -public class ChangeFeedService : IChangeFeedService -{ - private readonly IChangeFeedStore _changeFeedStore; - private readonly IMetadataStore _metadataStore; - private readonly RetrieveConfiguration _options; - - public ChangeFeedService( - IChangeFeedStore changeFeedStore, - IMetadataStore metadataStore, - IOptions options) - { - _changeFeedStore = EnsureArg.IsNotNull(changeFeedStore, nameof(changeFeedStore)); - _metadataStore = EnsureArg.IsNotNull(metadataStore, nameof(metadataStore)); - _options = EnsureArg.IsNotNull(options?.Value, nameof(metadataStore)); - } - - public async Task> GetChangeFeedAsync(TimeRange range, long offset, int limit, ChangeFeedOrder order, bool includeMetadata, CancellationToken cancellationToken = default) - { - if (offset < 0) - throw new ArgumentOutOfRangeException(nameof(offset)); - - if (limit < 1) - throw new ArgumentOutOfRangeException(nameof(limit)); - - IReadOnlyList changeFeedEntries = await _changeFeedStore.GetChangeFeedAsync(range, offset, limit, order, cancellationToken); - - if (includeMetadata) - { - await Parallel.ForEachAsync( - changeFeedEntries.Where(x => x.CurrentVersion.HasValue), - new ParallelOptions - { - CancellationToken = cancellationToken, - MaxDegreeOfParallelism = _options.MaxDegreeOfParallelism, - }, - async (entry, t) => - { - entry.IncludeMetadata = true; - entry.Metadata = await _metadataStore.GetInstanceMetadataAsync(entry.CurrentVersion.GetValueOrDefault(), t); - }); - } - - return changeFeedEntries; - } - - public async Task GetChangeFeedLatestAsync(ChangeFeedOrder order, bool includeMetadata, CancellationToken cancellationToken = default) - { - ChangeFeedEntry result = await _changeFeedStore.GetChangeFeedLatestAsync(order, cancellationToken); - - if (result == null) - return null; - - if (includeMetadata && result.CurrentVersion.HasValue) - { - result.IncludeMetadata = true; - result.Metadata = await _metadataStore.GetInstanceMetadataAsync(result.CurrentVersion.Value, cancellationToken); - } - - return result; - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/ChangeFeed/ChangeFeedState.cs b/src/Microsoft.Health.Dicom.Core/Features/ChangeFeed/ChangeFeedState.cs deleted file mode 100644 index f16009edb3..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/ChangeFeed/ChangeFeedState.cs +++ /dev/null @@ -1,13 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Features.ChangeFeed; - -public enum ChangeFeedState -{ - Current, - Replaced, - Deleted, -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/ChangeFeed/IChangeFeedService.cs b/src/Microsoft.Health.Dicom.Core/Features/ChangeFeed/IChangeFeedService.cs deleted file mode 100644 index df2d92a442..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/ChangeFeed/IChangeFeedService.cs +++ /dev/null @@ -1,18 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Health.Dicom.Core.Models; - -namespace Microsoft.Health.Dicom.Core.Features.ChangeFeed; - -public interface IChangeFeedService -{ - public Task> GetChangeFeedAsync(TimeRange range, long offset, int limit, ChangeFeedOrder order, bool includeMetadata, CancellationToken cancellationToken = default); - - public Task GetChangeFeedLatestAsync(ChangeFeedOrder order, bool includeMetadata, CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/ChangeFeed/IChangeFeedStore.cs b/src/Microsoft.Health.Dicom.Core/Features/ChangeFeed/IChangeFeedStore.cs deleted file mode 100644 index 7b18bf0cb3..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/ChangeFeed/IChangeFeedStore.cs +++ /dev/null @@ -1,18 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Health.Dicom.Core.Models; - -namespace Microsoft.Health.Dicom.Core.Features.ChangeFeed; - -public interface IChangeFeedStore -{ - Task> GetChangeFeedAsync(TimeRange range, long offset, int limit, ChangeFeedOrder order, CancellationToken cancellationToken = default); - - Task GetChangeFeedLatestAsync(ChangeFeedOrder order, CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Common/AsyncCache.cs b/src/Microsoft.Health.Dicom.Core/Features/Common/AsyncCache.cs deleted file mode 100644 index f639eab6c9..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Common/AsyncCache.cs +++ /dev/null @@ -1,79 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; - -namespace Microsoft.Health.Dicom.Features.Common; - -internal class AsyncCache : IDisposable -{ - private readonly Func> _factory; - private readonly SemaphoreSlim _semaphore; - private volatile object _initializing; - private bool _disposed; - private T _value; - - public AsyncCache(Func> factory) - { - _factory = EnsureArg.IsNotNull(factory, nameof(factory)); - _semaphore = new SemaphoreSlim(1, 1); - - // _initializing is a volatile reference type flag for determining whether init is needed. - // It is arbitrarily assigned _semaphore instead of allocating a new object - _initializing = _semaphore; - } - - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (!_disposed) - { - if (disposing) - { - _semaphore.Dispose(); - } - - _disposed = true; - } - } - - public async Task GetAsync(bool forceRefresh = false, CancellationToken cancellationToken = default) - { - if (_disposed) - { - throw new ObjectDisposedException(nameof(AsyncCache)); - } - - if (forceRefresh || _initializing is not null) - { - await _semaphore.WaitAsync(cancellationToken); - try - { -#pragma warning disable CA1508 - // Another thread may have already gone through this block - if (forceRefresh || _initializing is not null) -#pragma warning restore CA1508 - { - _value = await _factory(cancellationToken); - _initializing = null; // Volatile write must occur after _value - } - } - finally - { - _semaphore.Release(); - } - } - - return _value; - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Common/BaseHandler.cs b/src/Microsoft.Health.Dicom.Core/Features/Common/BaseHandler.cs deleted file mode 100644 index d85e9d39b7..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Common/BaseHandler.cs +++ /dev/null @@ -1,20 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using Microsoft.Health.Core.Features.Security.Authorization; -using Microsoft.Health.Dicom.Core.Features.Security; - -namespace Microsoft.Health.Dicom.Core.Features.Common; - -public abstract class BaseHandler -{ - protected BaseHandler(IAuthorizationService authorizationService) - { - AuthorizationService = EnsureArg.IsNotNull(authorizationService, nameof(authorizationService)); - } - - public IAuthorizationService AuthorizationService { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Common/DicomTagParser.cs b/src/Microsoft.Health.Dicom.Core/Features/Common/DicomTagParser.cs deleted file mode 100644 index 404957f563..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Common/DicomTagParser.cs +++ /dev/null @@ -1,93 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Globalization; -using System.Linq; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Validation; - -namespace Microsoft.Health.Dicom.Core.Features.Common; - -/// -/// Provides functionality to parse dicom tag path -/// -public class DicomTagParser : IDicomTagParser -{ - public bool TryParse(string dicomTagPath, out DicomTag[] dicomTags, bool supportMultiple = false) - { - dicomTags = null; - if (string.IsNullOrWhiteSpace(dicomTagPath)) - { - return false; - } - - DicomTag[] tags = dicomTagPath - .Split('.') - .Select(ParseTagFromKeywordOrNumber) - .ToArray(); - - if (!supportMultiple && tags.Length > 1) - { - throw new ElementValidationException(dicomTagPath, DicomVR.SQ, ValidationErrorCode.SequenceDisallowed); - } - - if (tags.Length > 2) - { - throw new ElementValidationException(dicomTagPath, DicomVR.SQ, ValidationErrorCode.NestedSequence); - } - - var result = tags.All(x => x != null); - dicomTags = result ? tags : null; - - return result; - } - - private static DicomTag ParseTagFromKeywordOrNumber(string dicomTagPath) - { - return ParseStardandDicomTagKeyword(dicomTagPath) ?? ParseDicomTagNumber(dicomTagPath); - } - - private static DicomTag ParseStardandDicomTagKeyword(string keyword) - { - // Try Keyword match, returns null if not found - DicomTag dicomTag = DicomDictionary.Default[keyword]; - - // We don't accept private tag from keyword - return (dicomTag != null && dicomTag.IsPrivate) ? null : dicomTag; - } - - private static DicomTag ParseDicomTagNumber(string s) - { - // When composed with number, length could only be 8 - if (s.Length != 8) - { - return null; - } - - if (!ushort.TryParse(s.AsSpan(0, 4), NumberStyles.HexNumber, null, out ushort group)) - { - return null; - } - - if (!ushort.TryParse(s.AsSpan(4, 4), NumberStyles.HexNumber, null, out ushort element)) - { - return null; - } - - var dicomTag = new DicomTag(group, element); - DicomDictionaryEntry knownTag = DicomDictionary.Default[dicomTag]; - - // Check if the tag is null or unknown. - // Tag with odd group is considered as private. - if (knownTag == null || (!dicomTag.IsPrivate && knownTag == DicomDictionary.UnknownTag)) - { - return null; - } - - return dicomTag; - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Common/EphemeralMemoryCache.cs b/src/Microsoft.Health.Dicom.Core/Features/Common/EphemeralMemoryCache.cs deleted file mode 100644 index c8515cfe18..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Common/EphemeralMemoryCache.cs +++ /dev/null @@ -1,128 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Configs; - -namespace Microsoft.Health.Dicom.Core.Features.Common; - -/// -/// Cache that stores a limited number of items for a limited amount of time. -/// -/// -/// -public abstract class EphemeralMemoryCache : IDisposable -{ - private readonly CacheConfiguration _configuration; - private readonly MemoryCache _memoryCache; - private readonly ILogger> _logger; - private readonly SemaphoreSlim _semaphore; - private bool _disposed; - - protected EphemeralMemoryCache( - IOptions configuration, - ILoggerFactory loggerFactory, - ILogger> logger) - { - EnsureArg.IsNotNull(configuration?.Value, nameof(configuration)); - EnsureArg.IsNotNull(loggerFactory, nameof(loggerFactory)); - EnsureArg.IsNotNull(logger, nameof(logger)); - - _configuration = configuration.Value; - _semaphore = new SemaphoreSlim(1, 1); - _memoryCache = new MemoryCache( - new MemoryCacheOptions - { - SizeLimit = _configuration.MaxCacheSize, - }, - loggerFactory); - _logger = logger; - } - - /// - /// Gets the cached value. If the asyncFactory returns null, the value returned is null. - /// - /// - /// - /// Make sure asyncFactory returns result and throws if any errors. Null values cannot be store - /// - /// - public async Task GetAsync( - object key, - TIn input, - Func> asyncFactory, - CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(key, nameof(key)); - EnsureArg.IsNotNull(asyncFactory, nameof(asyncFactory)); - - if (_memoryCache.TryGetValue(key, out TOut result)) - { - return result; - } - - // ideally we should lock the row that needs to be initialized. - // but compromising over multiple thread initializing the same row. - await _semaphore.WaitAsync(cancellationToken); - try - { - if (_memoryCache.TryGetValue(key, out result)) - { - return result; - } - - _logger.LogInformation("Cache miss. Initializing the row."); - - result = await asyncFactory(input, cancellationToken); - - // MemoryCache class does not allow null as a value - if (result == null) - { - return result; - } - - _memoryCache.Set( - key, - result, - new MemoryCacheEntryOptions - { - Size = 1, - AbsoluteExpirationRelativeToNow = new TimeSpan(0, _configuration.MaxCacheAbsoluteExpirationInMinutes, 0) - }); - } - finally - { - _semaphore.Release(); - } - - return result; - } - - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (!_disposed) - { - if (disposing) - { - _memoryCache.Dispose(); - _semaphore.Dispose(); - } - - _disposed = true; - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Common/FileProperties.cs b/src/Microsoft.Health.Dicom.Core/Features/Common/FileProperties.cs deleted file mode 100644 index bc5f0da6f2..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Common/FileProperties.cs +++ /dev/null @@ -1,15 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Features.Common; - -public class FileProperties -{ - public long ContentLength { get; init; } - - public string ETag { get; init; } - - public string Path { get; init; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Common/GuidFactory.cs b/src/Microsoft.Health.Dicom.Core/Features/Common/GuidFactory.cs deleted file mode 100644 index 62d3fefa75..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Common/GuidFactory.cs +++ /dev/null @@ -1,28 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; - -namespace Microsoft.Health.Dicom.Core.Features.Common; - -/// -/// Represents a factory that leverages for generating values. -/// -public sealed class GuidFactory : IGuidFactory -{ - private GuidFactory() - { - } - - /// - /// Gets the default that uses . - /// - /// The singleton instance. - public static IGuidFactory Default { get; } = new GuidFactory(); - - /// - public Guid Create() - => Guid.NewGuid(); // The default behavior unless we're testing and need a known value -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Common/IDicomTagParser.cs b/src/Microsoft.Health.Dicom.Core/Features/Common/IDicomTagParser.cs deleted file mode 100644 index c490903e8f..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Common/IDicomTagParser.cs +++ /dev/null @@ -1,23 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using FellowOakDicom; - -namespace Microsoft.Health.Dicom.Core.Features.Common; - -/// -/// Provides functionality to parse dicom tag path -/// -public interface IDicomTagParser -{ - /// - /// Parse dicom tag path. - /// - /// The dicom tag path. - /// The parsed dicom tags. - /// True to support multiple dicom tags like '00101002.00100024'. - /// True if succeed. - bool TryParse(string dicomTagPath, out DicomTag[] dicomTags, bool supportMultiple = false); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Common/IFileStore.cs b/src/Microsoft.Health.Dicom.Core/Features/Common/IFileStore.cs deleted file mode 100644 index 3092e94708..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Common/IFileStore.cs +++ /dev/null @@ -1,152 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Features.Partitioning; - -namespace Microsoft.Health.Dicom.Core.Features.Common; - -/// -/// Provides functionality to manage the DICOM files. -/// -public interface IFileStore -{ - /// - /// Asynchronously stores a file to the file store. - /// - /// The DICOM instance version. - /// Name of the partition - /// The DICOM instance stream. - /// The cancellation token. - /// A task that represents the asynchronous add operation. - Task StoreFileAsync(long version, string partitionName, Stream stream, CancellationToken cancellationToken = default); - - /// - /// Asynchronously gets a file from the file store. - /// - /// The DICOM instance version. - /// Partition to use when operating on file - /// blob file Properties - /// The cancellation token. - /// A task that represents the asynchronous get operation. - Task GetFileAsync(long version, Partition partition, FileProperties fileProperties, CancellationToken cancellationToken = default); - - /// - /// Asynchronously deletes a file from the file store if the file exists. - /// - /// The DICOM instance version. - /// Partition of the instance to be deleted - /// File properties of instance to be deleted - /// The cancellation token. - /// A task that represents the asynchronous delete operation. - Task DeleteFileIfExistsAsync(long version, Partition partition, FileProperties fileProperties, CancellationToken cancellationToken = default); - - /// - /// Asynchronously get file properties - /// - /// The DICOM instance version when file properties not known. - /// Partition of the instance to get file properties on when file properties not known - /// When file properties known, will use to get content length and match on etag - /// The cancellation token. - /// A task that represents the asynchronous get properties operation. - Task GetFilePropertiesAsync(long version, Partition partition, FileProperties fileProperties, CancellationToken cancellationToken = default); - - /// - /// Asynchronously get a specific range of bytes from the blob - /// - /// The DICOM instance version. - /// Partition within which the blob exists - /// Byte range in Httprange format with offset and length - /// File properties of blob to use to get it - /// The cancellation token. - /// Stream representing the bytes requested - Task GetFileFrameAsync( - long version, - Partition partition, - FrameRange range, - FileProperties fileProperties, - CancellationToken cancellationToken = default); - - /// - /// Asynchronously gets a streaming file from the file store. - /// - /// The DICOM instance version. - /// Partition within which the blob exists - /// File properties of blob to use to get it - /// The cancellation token. - /// A task that represents the asynchronous get operation. - Task GetStreamingFileAsync(long version, Partition partition, FileProperties fileProperties, CancellationToken cancellationToken = default); - - /// - /// Asynchronously stores a file to the file store in blocks. - /// - /// The DICOM instance version. - /// Partition to use when storing file - /// The DICOM instance stream. - /// This is the size of the stage block in bytes. - /// This is the ID and length of the initial block - /// The cancellation token. - /// A task that represents the asynchronous Store operation. - Task StoreFileInBlocksAsync(long version, Partition partition, Stream stream, int stageBlockSizeInBytes, KeyValuePair firstBlock, CancellationToken cancellationToken = default); - - /// - /// Asynchronously gets a file content from the file store. The file content will be in memory. Use only for small files - /// - /// The DICOM instance version. - /// Partition to use when operating on file - /// blob file Properties - /// Byte range in Httprange format with offset and length - /// The cancellation token. - /// - Task GetFileContentInRangeAsync(long version, Partition partition, FileProperties fileProperties, FrameRange range, CancellationToken cancellationToken = default); - - /// - /// Asynchronously updates a block in a blob. - /// - /// The DICOM instance version. - /// Partition to use when storing file - /// blob file Properties - /// BlockId to be updated. - /// The DICOM instance stream. - /// The cancellation token. - /// A task that represents the asynchronous update block operation. - Task UpdateFileBlockAsync(long version, Partition partition, FileProperties fileProperties, string blockId, Stream stream, CancellationToken cancellationToken = default); - - /// - /// Asynchronously gets a commited block list. - /// - /// The DICOM instance version. - /// Partition to use when storing file - /// Partition to use when operating on file - /// The cancellation token. - /// Key value pair of blockId and blockLength. - Task> GetFirstBlockPropertyAsync(long version, Partition partition, FileProperties fileProperties, CancellationToken cancellationToken = default); - - /// - /// Asynchronously copies file from the same container - /// - /// The DICOM instance original version. - /// The DICOM instance new version. - /// Partition to use when operating on file - /// blob file Properties - /// The cancellation token. - /// - Task CopyFileAsync(long originalVersion, long newVersion, Partition partition, FileProperties fileProperties, CancellationToken cancellationToken = default); - - /// - /// Asynchronously change blob access tier to cold tier. - /// - /// The DICOM instance version. - /// Partition to use when operating on file - /// Blob file Properties - /// The cancellation token. - /// - Task SetBlobToColdAccessTierAsync(long version, Partition partition, FileProperties fileProperties, CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Common/IGuidFactory.cs b/src/Microsoft.Health.Dicom.Core/Features/Common/IGuidFactory.cs deleted file mode 100644 index 579d8a1e55..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Common/IGuidFactory.cs +++ /dev/null @@ -1,20 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; - -namespace Microsoft.Health.Dicom.Core.Features.Common; - -/// -/// Represents a factory for generating unique values. -/// -public interface IGuidFactory -{ - /// - /// Creates a unique value. - /// - /// A unique value. - Guid Create(); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Common/IMetadataStore.cs b/src/Microsoft.Health.Dicom.Core/Features/Common/IMetadataStore.cs deleted file mode 100644 index 5e2d7fa9b5..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Common/IMetadataStore.cs +++ /dev/null @@ -1,90 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Features.Model; - -namespace Microsoft.Health.Dicom.Core.Features.Common; - -/// -/// Provides functionalities managing the DICOM instance metadata. -/// -public interface IMetadataStore -{ - /// - /// Asynchronously stores a DICOM instance metadata. - /// - /// The DICOM instance. - /// The version. - /// The cancellation token. - /// A task that represents the asynchronous add operation. - Task StoreInstanceMetadataAsync( - DicomDataset dicomDataset, - long version, - CancellationToken cancellationToken = default); - - /// - /// Asynchronously gets a DICOM instance metadata. - /// - /// The DICOM instance version. - /// The cancellation token. - /// A task that represents the asynchronous get operation. - Task GetInstanceMetadataAsync( - long version, - CancellationToken cancellationToken = default); - - /// - /// Asynchronously deletes a DICOM instance metadata. - /// - /// The DICOM instance version. - /// The cancellation token. - /// A task that represents the asynchronous delete operation. - Task DeleteInstanceMetadataIfExistsAsync( - long version, - CancellationToken cancellationToken = default); - - /// - /// Async store Frames range metadata - /// - /// The DICOM instance version. - /// Dictionary of frame id and byte range - /// The cancellation token. - /// A task that represents the async add of frame metadata - Task StoreInstanceFramesRangeAsync( - long version, - IReadOnlyDictionary framesRange, - CancellationToken cancellationToken = default); - - /// - /// Async get Frames range metadata - /// - /// The DICOM instance version. - /// The cancellation token. - /// Dictionary of frame id and byte range - Task> GetInstanceFramesRangeAsync( - long version, - CancellationToken cancellationToken = default); - - /// - /// Asynchronously deletes a DICOM instance frameRange metadata. - /// - /// The DICOM instance version. - /// The cancellation token. - /// A task that represents the asynchronous delete operation. - Task DeleteInstanceFramesRangeAsync( - long version, - CancellationToken cancellationToken = default); - - /// - /// Returns true if the frame range exist for the given version - /// - /// - /// - /// - Task DoesFrameRangeExistAsync(long version, CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Common/ISecretStore.cs b/src/Microsoft.Health.Dicom.Core/Features/Common/ISecretStore.cs deleted file mode 100644 index 110d96a6bf..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Common/ISecretStore.cs +++ /dev/null @@ -1,89 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.Health.Dicom.Core.Features.Common; - -/// -/// Represents a data store that securely stores named secrets. -/// -public interface ISecretStore -{ - /// - /// Asynchronously deletes the secret with the given name. - /// - /// The name of the secret. - /// - /// The token to monitor for cancellation requests. The default value is . - /// - /// - /// A task representing the operation. - /// The value of its property is if the - /// secret was successfully deleted; otherwise, if it does not exist. - /// - /// The was canceled. - Task DeleteSecretAsync(string name, CancellationToken cancellationToken = default); - - /// - /// Asynchronously retrieves the secret with the given name. - /// - /// The name of the secret. - /// The optional version of the secret. Defaults to the latest value. - /// - /// The token to monitor for cancellation requests. The default value is . - /// - /// - /// A task representing the operation. - /// The value of its property is value of the secret, if found; - /// otherwise, . - /// - /// The was canceled. - Task GetSecretAsync(string name, string version = null, CancellationToken cancellationToken = default); - - /// - /// Asynchronously lists all of the secrts in the store. - /// - /// - /// The token to monitor for cancellation requests. The default value is . - /// - /// The names of the secrets in the store. - /// The was canceled. - IAsyncEnumerable ListSecretsAsync(CancellationToken cancellationToken = default); - - /// - /// Asynchronously uploads the value for a secret. - /// - /// The name of the secret. - /// The value of the secret. - /// - /// The token to monitor for cancellation requests. The default value is . - /// - /// - /// A task representing the operation. - /// The value of its property is new version of the secret. - /// - /// The was canceled. - Task SetSecretAsync(string name, string value, CancellationToken cancellationToken = default); - - /// - /// Asynchronously uploads the value for a secret. - /// - /// The name of the secret. - /// The value of the secret. - /// The optional content type that describes the value. - /// - /// The token to monitor for cancellation requests. The default value is . - /// - /// - /// A task representing the operation. - /// The value of its property is new version of the secret. - /// - /// The was canceled. - Task SetSecretAsync(string name, string value, string contentType, CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Common/IndexedFileProperties.cs b/src/Microsoft.Health.Dicom.Core/Features/Common/IndexedFileProperties.cs deleted file mode 100644 index 62de77dbcf..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Common/IndexedFileProperties.cs +++ /dev/null @@ -1,38 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; - -namespace Microsoft.Health.Dicom.Core.Features.Common; - -/// -/// Metadata on FileProperty table in database -/// -public readonly struct IndexedFileProperties : IEquatable -{ - /// - /// Total indexed FileProperty in database - /// - public long TotalIndexed { get; init; } - - /// - /// Total sum of all ContentLength rows in FileProperty table - /// - public long TotalSum { get; init; } - - public override bool Equals(object obj) => obj is IndexedFileProperties other && Equals(other); - - public bool Equals(IndexedFileProperties other) - => TotalIndexed == other.TotalIndexed && TotalSum == other.TotalSum; - - public override int GetHashCode() - => HashCode.Combine(TotalIndexed, TotalSum); - - public static bool operator ==(IndexedFileProperties left, IndexedFileProperties right) - => left.Equals(right); - - public static bool operator !=(IndexedFileProperties left, IndexedFileProperties right) - => !(left == right); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Common/ParallelEnumerable.cs b/src/Microsoft.Health.Dicom.Core/Features/Common/ParallelEnumerable.cs deleted file mode 100644 index 0a8bb83847..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Common/ParallelEnumerable.cs +++ /dev/null @@ -1,90 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; -using EnsureThat; - -namespace Microsoft.Health.Dicom.Core.Features.Common; - -internal static class ParallelEnumerable -{ - public static async IAsyncEnumerable SelectParallel( - this IEnumerable source, - Func> selector, - ParallelEnumerationOptions options, - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - var state = new State(source, selector, options, cancellationToken); - ValueTask producer = ProduceAsync(state); - - await foreach (TResult item in state.Results.Reader.ReadAllAsync(cancellationToken)) - { - yield return item; - } - - await producer; - - static async ValueTask ProduceAsync(State state) - { - ChannelWriter writer = state.Results.Writer; - - try - { - // Note: The order is not deterministic. - // Items will be produced in the order that they are returned by the selector. - await Parallel.ForEachAsync( - state.Source, - state.Options, - async (item, token) => - { - if (await writer.WaitToWriteAsync(token)) - { - TResult result = await state.Selector(item, token); - await writer.WriteAsync(result, token); - } - }); - } - finally - { - writer.Complete(); - } - } - } - - private sealed class State - { - public State( - IEnumerable source, - Func> selector, - ParallelEnumerationOptions options, - CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(options, nameof(options)); - - Source = EnsureArg.IsNotNull(source, nameof(source)); - Selector = EnsureArg.IsNotNull(selector, nameof(selector)); - Options = new ParallelOptions - { - CancellationToken = cancellationToken, - MaxDegreeOfParallelism = options.MaxDegreeOfParallelism, - TaskScheduler = options.TaskScheduler, - }; - Results = Channel.CreateUnbounded(new UnboundedChannelOptions { SingleReader = true }); - } - - public Channel Results { get; } - - public ParallelOptions Options { get; } - - public Func> Selector { get; } - - public IEnumerable Source { get; } - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Common/ParallelEnumerationOptions.cs b/src/Microsoft.Health.Dicom.Core/Features/Common/ParallelEnumerationOptions.cs deleted file mode 100644 index ebe24f3301..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Common/ParallelEnumerationOptions.cs +++ /dev/null @@ -1,15 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading.Tasks; - -namespace Microsoft.Health.Dicom.Core.Features.Common; - -internal sealed class ParallelEnumerationOptions -{ - public int MaxDegreeOfParallelism { get; init; } = -1; - - public TaskScheduler TaskScheduler { get; init; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Context/DicomRequestContext.cs b/src/Microsoft.Health.Dicom.Core/Features/Context/DicomRequestContext.cs deleted file mode 100644 index 71c5690482..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Context/DicomRequestContext.cs +++ /dev/null @@ -1,94 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Security.Claims; -using EnsureThat; -using Microsoft.Extensions.Primitives; -using Microsoft.Health.Dicom.Core.Features.Partitioning; - -namespace Microsoft.Health.Dicom.Core.Features.Context; - -public class DicomRequestContext : IDicomRequestContext -{ - [Obsolete("Please use the other constructor.")] - [SuppressMessage("Design", "CA1054:URI-like parameters should not be strings", Justification = "Legacy constructor.")] - public DicomRequestContext( - string method, - string uriString, - string baseUriString, - string correlationId, - IDictionary requestHeaders, - IDictionary responseHeaders) - : this(method, new Uri(uriString), new Uri(baseUriString), correlationId, requestHeaders, responseHeaders) - { } - - public DicomRequestContext( - string method, - Uri uri, - Uri baseUri, - string correlationId, - IDictionary requestHeaders, - IDictionary responseHeaders) - { - EnsureArg.IsNotNullOrWhiteSpace(method, nameof(method)); - EnsureArg.IsNotNull(uri, nameof(uri)); - EnsureArg.IsNotNull(baseUri, nameof(baseUri)); - EnsureArg.IsNotNull(responseHeaders, nameof(responseHeaders)); - - Method = method; - Uri = uri; - BaseUri = baseUri; - CorrelationId = correlationId; - RequestHeaders = requestHeaders; - ResponseHeaders = responseHeaders; - DataPartition = Partition.Default; - } - - public string Method { get; } - - public Uri BaseUri { get; } - - public Uri Uri { get; } - - public string CorrelationId { get; } - - public string RouteName { get; set; } - - public string AuditEventType { get; set; } - - public Partition DataPartition { get; set; } - - public string StudyInstanceUid { get; set; } - - public string SeriesInstanceUid { get; set; } - - public string SopInstanceUid { get; set; } - - public bool IsTranscodeRequested { get; set; } - - public long BytesTranscoded { get; set; } - - public long BytesRendered { get; set; } - - public long ResponseSize { get; set; } - - public int PartCount { get; set; } - - public ClaimsPrincipal Principal { get; set; } - - public int Version { get; set; } - - public IDictionary RequestHeaders { get; } - - public IDictionary ResponseHeaders { get; } - - /// - /// Egress bytes from Dicom server to other resources - /// - public long TotalDicomEgressToStorageBytes { get; set; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Context/DicomRequestContextAccessor.cs b/src/Microsoft.Health.Dicom.Core/Features/Context/DicomRequestContextAccessor.cs deleted file mode 100644 index 2429d958d1..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Context/DicomRequestContextAccessor.cs +++ /dev/null @@ -1,21 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using Microsoft.Health.Core.Features.Context; - -namespace Microsoft.Health.Dicom.Core.Features.Context; - -public class DicomRequestContextAccessor : RequestContextAccessor, IDicomRequestContextAccessor -{ - private readonly AsyncLocal _dicomRequestContextCurrent = new AsyncLocal(); - - public override IDicomRequestContext RequestContext - { - get => _dicomRequestContextCurrent.Value; - - set => _dicomRequestContextCurrent.Value = value; - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Context/IDicomRequestContext.cs b/src/Microsoft.Health.Dicom.Core/Features/Context/IDicomRequestContext.cs deleted file mode 100644 index 7dfa7b83f6..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Context/IDicomRequestContext.cs +++ /dev/null @@ -1,36 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Microsoft.Health.Core.Features.Context; -using Microsoft.Health.Dicom.Core.Features.Partitioning; - -namespace Microsoft.Health.Dicom.Core.Features.Context; - -public interface IDicomRequestContext : IRequestContext -{ - string StudyInstanceUid { get; set; } - - string SeriesInstanceUid { get; set; } - - string SopInstanceUid { get; set; } - - bool IsTranscodeRequested { get; set; } - - long BytesTranscoded { get; set; } - - public long BytesRendered { get; set; } - - int PartCount { get; set; } - - Partition DataPartition { get; set; } - - // Opportunity for the core to change based on the caller version - int Version { get; set; } - - /// - /// Egress bytes from Dicom server to other resources - /// - long TotalDicomEgressToStorageBytes { get; set; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Context/IDicomRequestContextAccessor.cs b/src/Microsoft.Health.Dicom.Core/Features/Context/IDicomRequestContextAccessor.cs deleted file mode 100644 index 873bc9c376..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Context/IDicomRequestContextAccessor.cs +++ /dev/null @@ -1,11 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Features.Context; - -public interface IDicomRequestContextAccessor -{ - IDicomRequestContext RequestContext { get; set; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Delete/DeleteHandler.cs b/src/Microsoft.Health.Dicom.Core/Features/Delete/DeleteHandler.cs deleted file mode 100644 index ea7f393a56..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Delete/DeleteHandler.cs +++ /dev/null @@ -1,77 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Diagnostics; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using MediatR; -using Microsoft.Health.Core.Features.Security.Authorization; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Security; -using Microsoft.Health.Dicom.Core.Features.Validation; -using Microsoft.Health.Dicom.Core.Messages; -using Microsoft.Health.Dicom.Core.Messages.Delete; - -namespace Microsoft.Health.Dicom.Core.Features.Delete; - -public class DeleteHandler : BaseHandler, IRequestHandler -{ - private readonly IDeleteService _deleteService; - - public DeleteHandler(IAuthorizationService authorizationService, IDeleteService deleteService) - : base(authorizationService) - { - _deleteService = EnsureArg.IsNotNull(deleteService, nameof(deleteService)); - } - - /// - public async Task Handle(DeleteResourcesRequest request, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(request, nameof(request)); - - if (await AuthorizationService.CheckAccess(DataActions.Delete, cancellationToken) != DataActions.Delete) - { - throw new UnauthorizedDicomActionException(DataActions.Delete); - } - - ValidateDeleteResourcesRequest(request); - - switch (request.ResourceType) - { - case ResourceType.Study: - await _deleteService.DeleteStudyAsync(request.StudyInstanceUid, cancellationToken); - break; - case ResourceType.Series: - await _deleteService.DeleteSeriesAsync(request.StudyInstanceUid, request.SeriesInstanceUid, cancellationToken); - break; - case ResourceType.Instance: - await _deleteService.DeleteInstanceAsync(request.StudyInstanceUid, request.SeriesInstanceUid, request.SopInstanceUid, cancellationToken); - break; - default: - Debug.Fail($"Unknown delete transaction type: {request.ResourceType}", nameof(request)); - break; - } - - return new DeleteResourcesResponse(); - } - - private static void ValidateDeleteResourcesRequest(DeleteResourcesRequest request) - { - UidValidation.Validate(request.StudyInstanceUid, nameof(request.StudyInstanceUid)); - - switch (request.ResourceType) - { - case ResourceType.Series: - UidValidation.Validate(request.SeriesInstanceUid, nameof(request.SeriesInstanceUid)); - break; - case ResourceType.Instance: - UidValidation.Validate(request.SeriesInstanceUid, nameof(request.SeriesInstanceUid)); - UidValidation.Validate(request.SopInstanceUid, nameof(request.SopInstanceUid)); - break; - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Delete/DeleteService.cs b/src/Microsoft.Health.Dicom.Core/Features/Delete/DeleteService.cs deleted file mode 100644 index f441219b28..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Delete/DeleteService.cs +++ /dev/null @@ -1,267 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.ApplicationInsights; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Health.Abstractions.Features.Transactions; -#if !NET8_0_OR_GREATER -using Microsoft.Health.Core; -#endif -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Context; -using Microsoft.Health.Dicom.Core.Features.Diagnostic; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Core.Features.Store; -using Microsoft.Health.Dicom.Core.Models.Delete; - -namespace Microsoft.Health.Dicom.Core.Features.Delete; - -public class DeleteService : IDeleteService -{ - private readonly IDicomRequestContextAccessor _contextAccessor; - private readonly IIndexDataStore _indexDataStore; - private readonly IMetadataStore _metadataStore; - private readonly IFileStore _fileStore; - private readonly DeletedInstanceCleanupConfiguration _deletedInstanceCleanupConfiguration; - private readonly ITransactionHandler _transactionHandler; - private readonly ILogger _logger; - private readonly bool _isExternalStoreEnabled; - private readonly TelemetryClient _telemetryClient; - - private TimeSpan DeleteDelay => _isExternalStoreEnabled ? TimeSpan.Zero : _deletedInstanceCleanupConfiguration.DeleteDelay; - -#if NET8_0_OR_GREATER - private readonly TimeProvider _timeProvider; - - public DeleteService( - IIndexDataStore indexDataStore, - IMetadataStore metadataStore, - IFileStore fileStore, - IOptions deletedInstanceCleanupConfiguration, - ITransactionHandler transactionHandler, - ILogger logger, - IDicomRequestContextAccessor contextAccessor, - IOptions featureConfiguration, - TelemetryClient telemetryClient) - : this( - indexDataStore, - metadataStore, - fileStore, - deletedInstanceCleanupConfiguration, - transactionHandler, - logger, - contextAccessor, - featureConfiguration, - telemetryClient, - TimeProvider.System) - { } - - internal DeleteService( - IIndexDataStore indexDataStore, - IMetadataStore metadataStore, - IFileStore fileStore, - IOptions deletedInstanceCleanupConfiguration, - ITransactionHandler transactionHandler, - ILogger logger, - IDicomRequestContextAccessor contextAccessor, - IOptions featureConfiguration, - TelemetryClient telemetryClient, - TimeProvider timeProvider) - { - _timeProvider = EnsureArg.IsNotNull(timeProvider, nameof(timeProvider)); -#else - public DeleteService( - IIndexDataStore indexDataStore, - IMetadataStore metadataStore, - IFileStore fileStore, - IOptions deletedInstanceCleanupConfiguration, - ITransactionHandler transactionHandler, - ILogger logger, - IDicomRequestContextAccessor contextAccessor, - IOptions featureConfiguration, - TelemetryClient telemetryClient) - { -#endif - _indexDataStore = EnsureArg.IsNotNull(indexDataStore, nameof(indexDataStore)); - _metadataStore = EnsureArg.IsNotNull(metadataStore, nameof(metadataStore)); - _fileStore = EnsureArg.IsNotNull(fileStore, nameof(fileStore)); - _deletedInstanceCleanupConfiguration = EnsureArg.IsNotNull(deletedInstanceCleanupConfiguration?.Value, nameof(deletedInstanceCleanupConfiguration)); - _transactionHandler = EnsureArg.IsNotNull(transactionHandler, nameof(transactionHandler)); - _logger = EnsureArg.IsNotNull(logger, nameof(logger)); - _contextAccessor = EnsureArg.IsNotNull(contextAccessor, nameof(contextAccessor)); - _isExternalStoreEnabled = EnsureArg.IsNotNull(featureConfiguration?.Value, nameof(featureConfiguration)).EnableExternalStore; - _telemetryClient = EnsureArg.IsNotNull(telemetryClient, nameof(telemetryClient)); - } - - public async Task DeleteStudyAsync(string studyInstanceUid, CancellationToken cancellationToken) - { - DateTimeOffset cleanupAfter = GenerateCleanupAfter(DeleteDelay); - IReadOnlyCollection identifiers = await _indexDataStore.DeleteStudyIndexAsync(GetPartition(), studyInstanceUid, cleanupAfter, cancellationToken); - EmitTelemetry(identifiers); - } - - public async Task DeleteSeriesAsync(string studyInstanceUid, string seriesInstanceUid, CancellationToken cancellationToken) - { - DateTimeOffset cleanupAfter = GenerateCleanupAfter(DeleteDelay); - IReadOnlyCollection identifiers = await _indexDataStore.DeleteSeriesIndexAsync(GetPartition(), studyInstanceUid, seriesInstanceUid, cleanupAfter, cancellationToken); - EmitTelemetry(identifiers); - } - - public async Task DeleteInstanceAsync(string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid, CancellationToken cancellationToken) - { - DateTimeOffset cleanupAfter = GenerateCleanupAfter(DeleteDelay); - IReadOnlyCollection identifiers = await _indexDataStore.DeleteInstanceIndexAsync(GetPartition(), studyInstanceUid, seriesInstanceUid, sopInstanceUid, cleanupAfter, cancellationToken); - EmitTelemetry(identifiers); - } - - public Task DeleteInstanceNowAsync(string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid, CancellationToken cancellationToken) - { - return _indexDataStore.DeleteInstanceIndexAsync( - GetPartition(), - studyInstanceUid, - seriesInstanceUid, - sopInstanceUid, -#if NET8_0_OR_GREATER - _timeProvider.GetUtcNow(), -#else - Clock.UtcNow, -#endif - cancellationToken); - } - - [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Exceptions are captured for success return value.")] - public async Task<(bool Success, int RetrievedInstanceCount)> CleanupDeletedInstancesAsync(CancellationToken cancellationToken) - { - bool success = true; - int retrievedInstanceCount = 0; - - using (ITransactionScope transactionScope = _transactionHandler.BeginTransaction()) - { - try - { - var deletedInstanceIdentifiers = (await _indexDataStore - .RetrieveDeletedInstancesWithPropertiesAsync( - _deletedInstanceCleanupConfiguration.BatchSize, - _deletedInstanceCleanupConfiguration.MaxRetries, - cancellationToken)) - .ToList(); - - retrievedInstanceCount = deletedInstanceIdentifiers.Count; - - foreach (InstanceMetadata deletedInstanceIdentifier in deletedInstanceIdentifiers) - { - try - { - List tasks = new List() - { - _fileStore.DeleteFileIfExistsAsync( - deletedInstanceIdentifier.VersionedInstanceIdentifier.Version, - deletedInstanceIdentifier.VersionedInstanceIdentifier.Partition, - deletedInstanceIdentifier.InstanceProperties.FileProperties, - cancellationToken), - _metadataStore.DeleteInstanceMetadataIfExistsAsync( - deletedInstanceIdentifier.VersionedInstanceIdentifier.Version, - cancellationToken), - _metadataStore.DeleteInstanceFramesRangeAsync( - deletedInstanceIdentifier.VersionedInstanceIdentifier.Version, - cancellationToken) - }; - - // NOTE: in the input deletedInstanceIdentifiers we're going to have a row for each version in IDP, - // but for non-IDP we'll have a single row whose original version needs to be explicitly deleted below. - // To that end, we only need to delete by "original watermark" to catch changes from Update operation if not IDP. - if (!_isExternalStoreEnabled && deletedInstanceIdentifier.InstanceProperties.OriginalVersion.HasValue) - { - tasks.Add(_fileStore.DeleteFileIfExistsAsync(deletedInstanceIdentifier.InstanceProperties.OriginalVersion.Value, deletedInstanceIdentifier.VersionedInstanceIdentifier.Partition, deletedInstanceIdentifier.InstanceProperties.FileProperties, cancellationToken)); - tasks.Add(_metadataStore.DeleteInstanceMetadataIfExistsAsync(deletedInstanceIdentifier.InstanceProperties.OriginalVersion.Value, cancellationToken)); - } - - await Task.WhenAll(tasks); - - await _indexDataStore.DeleteDeletedInstanceAsync(deletedInstanceIdentifier.VersionedInstanceIdentifier, cancellationToken); - } - catch (Exception cleanupException) - { - try - { - int newRetryCount = await _indexDataStore.IncrementDeletedInstanceRetryAsync(deletedInstanceIdentifier.VersionedInstanceIdentifier, GenerateCleanupAfter(_deletedInstanceCleanupConfiguration.RetryBackOff), cancellationToken); - if (newRetryCount > _deletedInstanceCleanupConfiguration.MaxRetries) - { - _logger.LogCritical(cleanupException, "Failed to cleanup instance {DeletedInstanceIdentifier}. Retry count is now {NewRetryCount} and retry will not be re-attempted.", deletedInstanceIdentifier, newRetryCount); - } - else - { - _logger.LogError(cleanupException, "Failed to cleanup instance {DeletedInstanceIdentifier}. Retry count is now {NewRetryCount}.", deletedInstanceIdentifier, newRetryCount); - } - } - catch (Exception incrementException) - { - _logger.LogCritical(incrementException, "Failed to increment cleanup retry for instance {DeletedInstanceIdentifier}.", deletedInstanceIdentifier); - success = false; - } - } - } - - transactionScope.Complete(); - } - catch (Exception retrieveException) - { - _logger.LogCritical(retrieveException, "Failed to retrieve instances to cleanup."); - success = false; - } - } - - return (success, retrievedInstanceCount); - } - - public async Task GetMetricsAsync(CancellationToken cancellationToken = default) - { - Task oldestWaitingToBeDeleted = _indexDataStore.GetOldestDeletedAsync(cancellationToken); - Task numReachedMaxedRetry = _indexDataStore.RetrieveNumExhaustedDeletedInstanceAttemptsAsync( - _deletedInstanceCleanupConfiguration.MaxRetries, - cancellationToken); - - return new DeleteMetrics - { - OldestDeletion = await oldestWaitingToBeDeleted, - TotalExhaustedRetries = await numReachedMaxedRetry, - }; - } - - private void EmitTelemetry(IReadOnlyCollection identifiers) - { - _logger.LogInformation("Instances queued for deletion: {Count}", identifiers.Count); - _telemetryClient.ForwardLogTrace($"Instances queued for deletion: {identifiers.Count}"); - foreach (var identifier in identifiers) - { - _logger.LogInformation( - "Instance queued for deletion. Instance Watermark: {Watermark} , PartitionKey: {PartitionKey} , ExternalStore: {ExternalStore}", - identifier.Version, identifier.Partition.Key, _isExternalStoreEnabled); - _telemetryClient.ForwardLogTrace("Instance queued for deletion", identifier); - } - } - - private Partition GetPartition() - => _contextAccessor.RequestContext.GetPartition(); - -#if NET8_0_OR_GREATER - private DateTimeOffset GenerateCleanupAfter(TimeSpan delay) - => _timeProvider.GetUtcNow() + delay; -#else - private static DateTimeOffset GenerateCleanupAfter(TimeSpan delay) - => Clock.UtcNow + delay; -#endif -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Delete/IDeleteService.cs b/src/Microsoft.Health.Dicom.Core/Features/Delete/IDeleteService.cs deleted file mode 100644 index 8a4872126c..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Delete/IDeleteService.cs +++ /dev/null @@ -1,25 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Health.Dicom.Core.Models.Delete; - -namespace Microsoft.Health.Dicom.Core.Features.Delete; - -public interface IDeleteService -{ - Task DeleteStudyAsync(string studyInstanceUid, CancellationToken cancellationToken = default); - - Task DeleteSeriesAsync(string studyInstanceUid, string seriesInstanceUid, CancellationToken cancellationToken = default); - - Task DeleteInstanceAsync(string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid, CancellationToken cancellationToken = default); - - Task<(bool Success, int RetrievedInstanceCount)> CleanupDeletedInstancesAsync(CancellationToken cancellationToken = default); - - Task DeleteInstanceNowAsync(string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid, CancellationToken cancellationToken); - - Task GetMetricsAsync(CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Diagnostic/LogForwarderExtensions.cs b/src/Microsoft.Health.Dicom.Core/Features/Diagnostic/LogForwarderExtensions.cs deleted file mode 100644 index 7a28646da1..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Diagnostic/LogForwarderExtensions.cs +++ /dev/null @@ -1,165 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using EnsureThat; -using Microsoft.ApplicationInsights; -using Microsoft.ApplicationInsights.DataContracts; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Features.Partitioning; - -namespace Microsoft.Health.Dicom.Core.Features.Diagnostic; - -/// -/// A log forwarder which sets a property designating a log to be forwarded. -/// -internal static class LogForwarderExtensions -{ - private const int MaxShoeboxPropertySize = 32 * 1024; - private const string ForwardLogFlag = "forwardLog"; - private const string Prefix = "dicomAdditionalInformation_"; - private const string OperationName = "operationName"; - private const string StudyInstanceUID = $"{Prefix}studyInstanceUID"; - private const string SeriesInstanceUID = $"{Prefix}seriesInstanceUID"; - private const string SOPInstanceUID = $"{Prefix}sopInstanceUID"; - private const string PartitionName = $"{Prefix}partitionName"; - private const string InputPayload = $"{Prefix}input"; - private const string OperationId = $"{Prefix}operationId"; - private const string FilePropertiesETag = $"{Prefix}filePropertiesETag"; - private const string FilePropertiesPath = $"{Prefix}filePropertiesPath"; - - /// - /// Emits a trace log with forwarding flag set and adds properties from instanceIdentifier as properties to telemetry. - /// - /// client to use to emit the trace - /// message to set on the trace log - /// identifier to use to set UIDs on log and telemetry properties - /// Severity level of the message - public static void ForwardLogTrace( - this TelemetryClient telemetryClient, - string message, - InstanceIdentifier instanceIdentifier, - SeverityLevel severityLevel = SeverityLevel.Information) - { - EnsureArg.IsNotNull(telemetryClient, nameof(telemetryClient)); - EnsureArg.IsNotNull(message, nameof(message)); - EnsureArg.IsNotNull(instanceIdentifier, nameof(instanceIdentifier)); - - var telemetry = new TraceTelemetry(message, severityLevel); - telemetry.Properties.Add(StudyInstanceUID, instanceIdentifier.StudyInstanceUid); - telemetry.Properties.Add(SeriesInstanceUID, instanceIdentifier.SeriesInstanceUid); - telemetry.Properties.Add(SOPInstanceUID, instanceIdentifier.SopInstanceUid); - telemetry.Properties.Add(ForwardLogFlag, bool.TrueString); - if (instanceIdentifier.Partition != Partition.Default) - { - telemetry.Properties.Add(PartitionName, instanceIdentifier.Partition.Name); - } - - telemetryClient.TrackTrace(telemetry); - } - - /// - /// Emits a trace log with forwarding flag set. - /// - /// NOTE - do not use this if reporting on any specific instance. Only use as high level remarks. Attempt to use identifiers wherever possible - /// client to use to emit the trace - /// message to set on the trace log - /// Severity level of the message - public static void ForwardLogTrace( - this TelemetryClient telemetryClient, - string message, - SeverityLevel severityLevel = SeverityLevel.Information) - { - EnsureArg.IsNotNull(telemetryClient, nameof(telemetryClient)); - EnsureArg.IsNotNull(message, nameof(message)); - - var telemetry = new TraceTelemetry(message, severityLevel); - telemetry.Properties.Add(ForwardLogFlag, bool.TrueString); - - telemetryClient.TrackTrace(telemetry); - } - - /// - /// Emits a trace log regarding operations on a specified file with forwarding flag set. - /// - /// client to use to emit the trace - /// message to set on the trace log - /// Partition within which file is residing - /// File properties of file this message is regarding - /// Severity level of the message - public static void ForwardLogTrace( - this TelemetryClient telemetryClient, - string message, - Partition partition, - FileProperties fileProperties, - SeverityLevel severityLevel = SeverityLevel.Information) - { - EnsureArg.IsNotNull(telemetryClient, nameof(telemetryClient)); - EnsureArg.IsNotNull(message, nameof(message)); - EnsureArg.IsNotNull(partition, nameof(partition)); - EnsureArg.IsNotNull(fileProperties, nameof(fileProperties)); - - var telemetry = new TraceTelemetry(message, severityLevel); - telemetry.Properties.Add(ForwardLogFlag, bool.TrueString); - telemetry.Properties.Add(FilePropertiesPath, fileProperties.Path); - telemetry.Properties.Add(FilePropertiesETag, fileProperties.ETag); - if (partition != Partition.Default) - { - telemetry.Properties.Add(PartitionName, partition.Name); - } - - telemetryClient.TrackTrace(telemetry); - } - - /// - /// Emits a trace log with forwarding flag set for operations and adds the required properties to telemetry. - /// For Audit shoebox the operation name are automatically populated from HttpContext. For internal operation, the operation name needs to be passed in. - /// - /// client to use to emit the trace - /// message to set on the trace log - /// operation id - /// Input payload to pass to the forward logger - /// Operation name of the trace event - /// Severity level of the message - public static void ForwardOperationLogTrace( - this TelemetryClient telemetryClient, - string message, - string operationId, - string input, - string operationName, - SeverityLevel severityLevel = SeverityLevel.Information) - { - EnsureArg.IsNotNull(telemetryClient, nameof(telemetryClient)); - EnsureArg.IsNotNull(message, nameof(message)); - EnsureArg.IsNotNull(operationId, nameof(operationId)); - - // Shoebox property size has a limitation of 32 KB which is why the diagnostic log is split into multiple messages - int startIndex = 0, inputSize = input.Length; - while (startIndex < inputSize) - { - int offset = Math.Min(MaxShoeboxPropertySize, input.Length - startIndex); - ForwardOperationLogTraceWithSizeLimit(telemetryClient, message, operationId, input.Substring(startIndex, offset), operationName, severityLevel); - startIndex += offset; - } - } - - private static void ForwardOperationLogTraceWithSizeLimit( - TelemetryClient telemetryClient, - string message, - string operationId, - string input, - string operationName, - SeverityLevel severityLevel) - { - var telemetry = new TraceTelemetry(message, severityLevel); - telemetry.Properties.Add(InputPayload, input); - telemetry.Properties.Add(OperationId, operationId); - telemetry.Properties.Add(ForwardLogFlag, bool.TrueString); - telemetry.Properties.Add(OperationName, operationName); - - telemetryClient.TrackTrace(telemetry); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Export/CopyFailureEventArgs.cs b/src/Microsoft.Health.Dicom.Core/Features/Export/CopyFailureEventArgs.cs deleted file mode 100644 index eda5a0a83d..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Export/CopyFailureEventArgs.cs +++ /dev/null @@ -1,42 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using EnsureThat; -using Microsoft.Health.Dicom.Core.Features.Model; - -namespace Microsoft.Health.Dicom.Core.Features.Export; - -/// -/// Provides data for the event . -/// -public sealed class CopyFailureEventArgs : EventArgs -{ - /// - /// Gets the identifier for the DICOM file that failed to copy. - /// - /// The versioned instance identifier including its watermark. - public VersionedInstanceIdentifier Identifier { get; } - - /// - /// Gets the exception that caused the failure. - /// - /// An instance of the class. - public Exception Exception { get; } - - /// - /// Initializes a new instance of the class. - /// - /// An identifier for the DICOM file that failed to copy. - /// The exception raised by the copy failure. - /// - /// or is . - /// - public CopyFailureEventArgs(VersionedInstanceIdentifier identifier, Exception exception) - { - Identifier = EnsureArg.IsNotNull(identifier, nameof(identifier)); - Exception = EnsureArg.IsNotNull(exception, nameof(exception)); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Export/ExportHandler.cs b/src/Microsoft.Health.Dicom.Core/Features/Export/ExportHandler.cs deleted file mode 100644 index 5007e81a17..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Export/ExportHandler.cs +++ /dev/null @@ -1,37 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using MediatR; -using Microsoft.Health.Core.Features.Security.Authorization; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Security; -using Microsoft.Health.Dicom.Core.Messages.Export; - -namespace Microsoft.Health.Dicom.Core.Features.Export; - -internal class ExportHandler : BaseHandler, IRequestHandler -{ - private readonly IExportService _service; - - public ExportHandler(IAuthorizationService authorizationService, IExportService exportService) - : base(authorizationService) - => _service = EnsureArg.IsNotNull(exportService, nameof(exportService)); - - public async Task Handle(ExportRequest request, CancellationToken cancellationToken = default) - { - EnsureArg.IsNotNull(request, nameof(request)); - - if (await AuthorizationService.CheckAccess(DataActions.Export, cancellationToken) != DataActions.Export) - { - throw new UnauthorizedDicomActionException(DataActions.Export); - } - - return new ExportResponse(await _service.StartExportAsync(request.Specification, cancellationToken)); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Export/ExportService.cs b/src/Microsoft.Health.Dicom.Core/Features/Export/ExportService.cs deleted file mode 100644 index 0d27165cdc..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Export/ExportService.cs +++ /dev/null @@ -1,65 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Context; -using Microsoft.Health.Dicom.Core.Features.Operations; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Core.Models.Export; -using Microsoft.Health.Operations; - -namespace Microsoft.Health.Dicom.Core.Features.Export; - -internal sealed class ExportService : IExportService -{ - private readonly ExportSourceFactory _sourceFactory; - private readonly ExportSinkFactory _sinkFactory; - private readonly IGuidFactory _guidFactory; - private readonly IDicomOperationsClient _client; - private readonly IDicomRequestContextAccessor _accessor; - - public ExportService( - ExportSourceFactory sourceFactory, - ExportSinkFactory sinkFactory, - IGuidFactory guidFactory, - IDicomOperationsClient client, - IDicomRequestContextAccessor requestContextAccessor) - { - _sourceFactory = EnsureArg.IsNotNull(sourceFactory, nameof(sourceFactory)); - _sinkFactory = EnsureArg.IsNotNull(sinkFactory, nameof(sinkFactory)); - _guidFactory = EnsureArg.IsNotNull(guidFactory, nameof(guidFactory)); - _client = EnsureArg.IsNotNull(client, nameof(client)); - _accessor = EnsureArg.IsNotNull(requestContextAccessor, nameof(requestContextAccessor)); - } - - public async Task StartExportAsync(ExportSpecification specification, CancellationToken cancellationToken = default) - { - EnsureArg.IsNotNull(specification, nameof(specification)); - - Guid operationId = _guidFactory.Create(); - - // Validate the input and update the specification - await _sourceFactory.ValidateAsync(specification.Source, cancellationToken); - await _sinkFactory.ValidateAsync(specification.Destination, cancellationToken); - - // Initialize the sink before running the operation to ensure we can connect - await using IExportSink sink = await _sinkFactory.CreateAsync(specification.Destination, operationId, cancellationToken); - Uri errorHref = await sink.InitializeAsync(cancellationToken); - - specification = new ExportSpecification - { - Source = specification.Source, - Destination = await _sinkFactory.SecureSensitiveInfoAsync(specification.Destination, operationId, cancellationToken), - }; - - // Start the operation - Partition partition = _accessor.RequestContext.DataPartition; - return await _client.StartExportAsync(operationId, specification, errorHref, partition, cancellationToken); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Export/ExportSinkFactory.cs b/src/Microsoft.Health.Dicom.Core/Features/Export/ExportSinkFactory.cs deleted file mode 100644 index 11ca45420d..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Export/ExportSinkFactory.cs +++ /dev/null @@ -1,131 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Globalization; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.Health.Dicom.Core.Models.Export; - -namespace Microsoft.Health.Dicom.Core.Features.Export; - -/// -/// Represents a factory that creates instances based on the configured providers. -/// -public sealed class ExportSinkFactory -{ - private readonly Dictionary _providers; - - /// - /// Initializes a new instance of the class. - /// - /// A collection of sink providers. - /// - /// Two or more providers have the same value for their property. - /// - /// is . - public ExportSinkFactory(IEnumerable providers) - => _providers = EnsureArg.IsNotNull(providers, nameof(providers)).ToDictionary(x => x.Type); - - /// - /// Asynchronously completes a copy operation to the sink. - /// - /// The options for a specific sink type. - /// - /// The token to monitor for cancellation requests. The default value is . - /// - /// A task representing the operation. - /// is . - /// - /// There is no provider configured for the value of the property. - /// - /// The was canceled. - public Task CompleteCopyAsync(ExportDataOptions destination, CancellationToken cancellationToken = default) - => GetProvider(EnsureArg.IsNotNull(destination, nameof(destination)).Type) - .CompleteCopyAsync(destination.Settings, cancellationToken); - - /// - /// Asynchronously creates a new instance of the interface whose implementation - /// is based on given . - /// - /// The options for a specific sink type. - /// The ID for the export operation. - /// - /// The token to monitor for cancellation requests. The default value is . - /// - /// - /// A task representing the operation. - /// The value of its property is the corresponding - /// instance - /// - /// is . - /// - /// There is no provider configured for the value of the property. - /// - /// The was canceled. - public Task CreateAsync(ExportDataOptions destination, Guid operationId, CancellationToken cancellationToken = default) - => GetProvider(EnsureArg.IsNotNull(destination, nameof(destination)).Type).CreateAsync(destination.Settings, operationId, cancellationToken); - - /// - /// Asynchronously stores sensitive information in a secure format and returns the updated options. - /// - /// - /// It is the responsibility of the method to retrieve any sensitive information - /// that was secured by this method. - /// - /// The options for a specific sink type. - /// The ID for the export operation. - /// - /// The token to monitor for cancellation requests. The default value is . - /// - /// - /// A task representing the operation. - /// The value of its property is a new options object with any sensitive - /// information secured. - /// - /// is . - /// - /// There is no provider configured for the value of the property. - /// - /// The was canceled. - public async Task> SecureSensitiveInfoAsync(ExportDataOptions destination, Guid operationId, CancellationToken cancellationToken = default) - { - EnsureArg.IsNotNull(destination, nameof(destination)); - - IExportSinkProvider provider = GetProvider(destination.Type); - return new ExportDataOptions( - destination.Type, - await provider.SecureSensitiveInfoAsync(destination.Settings, operationId, cancellationToken)); - } - - /// - /// Asynchronously ensures that the given options can be used to create a valid sink. - /// - /// The options for a specific sink type. - /// - /// The token to monitor for cancellation requests. The default value is . - /// - /// A task representing the operation. - /// is . - /// - /// There is no provider configured for the value of the property. - /// - /// The was canceled. - /// There were one or more problems with the sink-specific options. - public async Task ValidateAsync(ExportDataOptions destination, CancellationToken cancellationToken = default) - { - EnsureArg.IsNotNull(destination, nameof(destination)); - await GetProvider(destination.Type).ValidateAsync(destination.Settings, cancellationToken); - } - - private IExportSinkProvider GetProvider(ExportDestinationType type) - => _providers.TryGetValue(type, out IExportSinkProvider provider) - ? provider - : throw new KeyNotFoundException(string.Format(CultureInfo.CurrentCulture, DicomCoreResource.UnsupportedExportDestination, type)); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Export/ExportSinkProvider.cs b/src/Microsoft.Health.Dicom.Core/Features/Export/ExportSinkProvider.cs deleted file mode 100644 index 4b1e4f65fd..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Export/ExportSinkProvider.cs +++ /dev/null @@ -1,49 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.Health.Dicom.Core.Models.Export; - -namespace Microsoft.Health.Dicom.Core.Features.Export; - -internal abstract class ExportSinkProvider : IExportSinkProvider -{ - public abstract ExportDestinationType Type { get; } - - public Task CompleteCopyAsync(object options, CancellationToken cancellationToken = default) - { - EnsureArg.IsNotNull(options, nameof(options)); - return CompleteCopyAsync((TOptions)options, cancellationToken); - } - - public Task CreateAsync(object options, Guid operationId, CancellationToken cancellationToken = default) - { - EnsureArg.IsNotNull(options, nameof(options)); - return CreateAsync((TOptions)options, operationId, cancellationToken); - } - - public async Task SecureSensitiveInfoAsync(object options, Guid operationId, CancellationToken cancellationToken = default) - { - EnsureArg.IsNotNull(options, nameof(options)); - return await SecureSensitiveInfoAsync((TOptions)options, operationId, cancellationToken); - } - - public Task ValidateAsync(object options, CancellationToken cancellationToken = default) - { - EnsureArg.IsNotNull(options, nameof(options)); - return ValidateAsync((TOptions)options, cancellationToken); - } - - protected abstract Task CompleteCopyAsync(TOptions options, CancellationToken cancellationToken = default); - - protected abstract Task CreateAsync(TOptions options, Guid operationId, CancellationToken cancellationToken = default); - - protected abstract Task SecureSensitiveInfoAsync(TOptions options, Guid operationId, CancellationToken cancellationToken = default); - - protected abstract Task ValidateAsync(TOptions options, CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Export/ExportSourceFactory.cs b/src/Microsoft.Health.Dicom.Core/Features/Export/ExportSourceFactory.cs deleted file mode 100644 index 70a154fb7a..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Export/ExportSourceFactory.cs +++ /dev/null @@ -1,85 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Globalization; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Core.Models.Export; - -namespace Microsoft.Health.Dicom.Core.Features.Export; - -/// -/// Represents a factory that creates instances based on the configured providers. -/// -public sealed class ExportSourceFactory -{ - private readonly Dictionary _providers; - - /// - /// Initializes a new instance of the class. - /// - /// A collection of source providers. - /// - /// Two or more providers have the same value for their property. - /// - /// is . - public ExportSourceFactory(IEnumerable providers) - => _providers = EnsureArg.IsNotNull(providers, nameof(providers)).ToDictionary(x => x.Type); - - /// - /// Asynchronously creates a new instance of the interface whose implementation - /// is based on given . - /// - /// The options for a specific source type. - /// The data partition. - /// - /// The token to monitor for cancellation requests. The default value is . - /// - /// - /// A task representing the operation. - /// The value of its property is the corresponding - /// instance - /// - /// - /// or is . - /// - /// - /// There is no provider configured for the value of the property. - /// - /// The was canceled. - public Task CreateAsync(ExportDataOptions source, Partition partition, CancellationToken cancellationToken = default) - => GetProvider(EnsureArg.IsNotNull(source, nameof(source)).Type).CreateAsync(source.Settings, partition, cancellationToken); - - /// - /// Asynchronously ensures that the given options can be used to create a valid source. - /// - /// The options for a specific source type. - /// - /// The token to monitor for cancellation requests. The default value is . - /// - /// A task representing the operation. - /// is . - /// - /// There is no provider configured for the value of the property. - /// - /// The was canceled. - /// There were one or more problems with the source-specific options. - public async Task ValidateAsync(ExportDataOptions source, CancellationToken cancellationToken = default) - { - EnsureArg.IsNotNull(source, nameof(source)); - await GetProvider(source.Type).ValidateAsync(source.Settings, cancellationToken); - } - - private IExportSourceProvider GetProvider(ExportSourceType type) - => _providers.TryGetValue(type, out IExportSourceProvider provider) - ? provider - : throw new KeyNotFoundException(string.Format(CultureInfo.CurrentCulture, DicomCoreResource.UnsupportedExportSource, type)); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Export/ExportSourceProvider.cs b/src/Microsoft.Health.Dicom.Core/Features/Export/ExportSourceProvider.cs deleted file mode 100644 index 367ec6195d..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Export/ExportSourceProvider.cs +++ /dev/null @@ -1,34 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Core.Models.Export; - -namespace Microsoft.Health.Dicom.Core.Features.Export; - -internal abstract class ExportSourceProvider : IExportSourceProvider -{ - public abstract ExportSourceType Type { get; } - - public Task CreateAsync(object options, Partition partition, CancellationToken cancellationToken = default) - { - EnsureArg.IsNotNull(options, nameof(options)); - EnsureArg.IsNotNull(partition, nameof(partition)); - return CreateAsync((TOptions)options, partition, cancellationToken); - } - - public Task ValidateAsync(object options, CancellationToken cancellationToken = default) - { - EnsureArg.IsNotNull(options, nameof(options)); - return ValidateAsync((TOptions)options, cancellationToken); - } - - protected abstract Task CreateAsync(TOptions options, Partition partition, CancellationToken cancellationToken = default); - - protected abstract Task ValidateAsync(TOptions options, CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Export/IExportService.cs b/src/Microsoft.Health.Dicom.Core/Features/Export/IExportService.cs deleted file mode 100644 index a5829abbd7..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Export/IExportService.cs +++ /dev/null @@ -1,16 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Health.Dicom.Core.Models.Export; -using Microsoft.Health.Operations; - -namespace Microsoft.Health.Dicom.Core.Features.Export; - -internal interface IExportService -{ - Task StartExportAsync(ExportSpecification specification, CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Export/IExportSink.cs b/src/Microsoft.Health.Dicom.Core/Features/Export/IExportSink.cs deleted file mode 100644 index b1edbb254d..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Export/IExportSink.cs +++ /dev/null @@ -1,80 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Health.Dicom.Core.Exceptions; - -namespace Microsoft.Health.Dicom.Core.Features.Export; - -/// -/// Represents a destination for export operations into which file may be copied. -/// -public interface IExportSink : IAsyncDisposable -{ - /// - /// Occurs when a file fails to copy. - /// - event EventHandler CopyFailure; - - /// - /// Asychronously initializes the sink for copying. - /// - /// - /// Initialization will create any resources needed for copying and can be used to assess - /// whether ther sink has been configured correctly. - /// - /// - /// The token to monitor for cancellation requests. The default value is . - /// - /// - /// A task representing the operation. The value of the - /// property contains the for the error log. - /// - /// The was canceled. - /// The sink failed to initialize. - Task InitializeAsync(CancellationToken cancellationToken = default); - - /// - /// Asynchronously copies the given into the destination. - /// - /// - /// - /// The may represent either a DICOM file or an error generated - /// by a corresponding source. Files and errors are typically written to different locations - /// within the destination. - /// - /// - /// This method is thread-safe. - /// - /// - /// The result of a previous read operation. May be an identifier or an error. - /// - /// The token to monitor for cancellation requests. The default value is . - /// - /// - /// A task representing the operation. - /// The value of its property is if the operation - /// succeeded; otherwise, . - /// - /// The was canceled. - Task CopyAsync(ReadResult value, CancellationToken cancellationToken = default); - - /// - /// Asynchronously flushes any buffered content into the destination. - /// - /// - /// This method is not thread-safe. - /// - /// - /// The token to monitor for cancellation requests. The default value is . - /// - /// - /// A task representing the operation. - /// - /// The was canceled. - Task FlushAsync(CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Export/IExportSinkProvider.cs b/src/Microsoft.Health.Dicom.Core/Features/Export/IExportSinkProvider.cs deleted file mode 100644 index 1af4d93547..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Export/IExportSinkProvider.cs +++ /dev/null @@ -1,89 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.ComponentModel.DataAnnotations; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Health.Dicom.Core.Models.Export; - -namespace Microsoft.Health.Dicom.Core.Features.Export; - -/// -/// Represents a provider of instances indicated by the value -/// of the property. -/// -public interface IExportSinkProvider -{ - /// - /// Gets the type of the sink produced by this provider. - /// - /// A value that represents the type of associated instances. - ExportDestinationType Type { get; } - - /// - /// Asynchronously completes a copy operation to the sink. - /// - /// The sink-specific options. - /// - /// The token to monitor for cancellation requests. The default value is . - /// - /// A task representing the operation. - /// is . - /// The was canceled. - public Task CompleteCopyAsync(object options, CancellationToken cancellationToken = default); - - /// - /// Asynchronously creates a new instance of the interface whose implementation - /// is based on the value of the property. - /// - /// The sink-specific options. - /// The ID for the export operation. - /// - /// The token to monitor for cancellation requests. The default value is . - /// - /// - /// A task representing the operation. - /// The value of its property is the corresponding - /// instance of the interface. - /// - /// is . - /// The was canceled. - Task CreateAsync(object options, Guid operationId, CancellationToken cancellationToken = default); - - /// - /// Asynchronously stores sensitive information in a secure format and returns the updated configuration. - /// - /// - /// It is the responsibility of the method to retrieve any sensitive information - /// that was secured by this method. - /// - /// The sink-specific options. - /// The ID for the export operation. - /// - /// The token to monitor for cancellation requests. The default value is . - /// - /// - /// A task representing the operation. - /// The value of its property is a new options instance with any sensitive - /// information secured. - /// - /// is . - /// The was canceled. - Task SecureSensitiveInfoAsync(object options, Guid operationId, CancellationToken cancellationToken = default); - - /// - /// Asynchronously ensures that the given can be used to create a valid sink. - /// - /// The sink-specific options. - /// - /// The token to monitor for cancellation requests. The default value is . - /// - /// A task representing the operation. - /// is . - /// The was canceled. - /// There were one or more problems with the . - Task ValidateAsync(object options, CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Export/IExportSource.cs b/src/Microsoft.Health.Dicom.Core/Features/Export/IExportSource.cs deleted file mode 100644 index dcab632d7c..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Export/IExportSource.cs +++ /dev/null @@ -1,47 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using Microsoft.Health.Dicom.Core.Models.Export; - -namespace Microsoft.Health.Dicom.Core.Features.Export; - -/// -/// Represents a source for export operations from which files may be read. -/// -public interface IExportSource : IAsyncEnumerable, IAsyncDisposable -{ - /// - /// Occurs when a study, series, or SOP instance fails to be read. - /// - event EventHandler ReadFailure; - - /// - /// Gets the options that describe the current state of the source. - /// - /// Options that represents the source. - ExportDataOptions Description { get; } - - /// - /// Attempts to dequeue a subset of the source's elements such that a new source may - /// be created from the resulting options that contains the dequeued batch. - /// - /// - /// Batches may contain more and less elements depending on how many files remain - /// in the source and on the implementation. - /// - /// The size of the desired batch. - /// - /// When this method returns, the value resulting batch, if there is any data left; - /// otherwise, the default value for the type of the parameter. - /// This parameter is passed uninitialized. - /// - /// - /// if the source contains any elements; otherwise . - /// - /// is less than 1. - bool TryDequeueBatch(int size, out ExportDataOptions batch); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Export/IExportSourceProvider.cs b/src/Microsoft.Health.Dicom.Core/Features/Export/IExportSourceProvider.cs deleted file mode 100644 index deaa3bf958..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Export/IExportSourceProvider.cs +++ /dev/null @@ -1,59 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.ComponentModel.DataAnnotations; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Core.Models.Export; - -namespace Microsoft.Health.Dicom.Core.Features.Export; - -/// -/// Represents a provider of instances indicated by the value -/// of the property. -/// -public interface IExportSourceProvider -{ - /// - /// Gets the type of the source produced by this provider. - /// - /// A value that represents the type of associated instances. - ExportSourceType Type { get; } - - /// - /// Asynchronously creates a new instance of the interface whose implementation - /// is based on the value of the property. - /// - /// The source-specific options. - /// The data partition. - /// - /// The token to monitor for cancellation requests. The default value is . - /// - /// - /// A task representing the operation. - /// The value of its property is the corresponding - /// instance of the interface. - /// - /// - /// or is . - /// - /// The was canceled. - Task CreateAsync(object options, Partition partition, CancellationToken cancellationToken = default); - - /// - /// Asynchronously ensures that the given can be used to create a valid source. - /// - /// The source-specific options. - /// - /// The token to monitor for cancellation requests. The default value is . - /// - /// A task representing the operation. - /// is . - /// The was canceled. - /// There were one or more problems with the . - Task ValidateAsync(object options, CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Export/IdentifierExportSource.cs b/src/Microsoft.Health.Dicom.Core/Features/Export/IdentifierExportSource.cs deleted file mode 100644 index 389b4802d3..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Export/IdentifierExportSource.cs +++ /dev/null @@ -1,98 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Core.Features.Retrieve; -using Microsoft.Health.Dicom.Core.Messages; -using Microsoft.Health.Dicom.Core.Models.Common; -using Microsoft.Health.Dicom.Core.Models.Export; - -namespace Microsoft.Health.Dicom.Core.Features.Export; - -internal sealed class IdentifierExportSource : IExportSource -{ - public event EventHandler ReadFailure; - - public ExportDataOptions Description => _identifiers.Count > 0 ? CreateOptions(_identifiers) : null; - - private readonly IInstanceStore _instanceStore; - private readonly Partition _partition; - private readonly Queue _identifiers; - - public IdentifierExportSource(IInstanceStore instanceStore, Partition partition, IdentifierExportOptions options) - { - _instanceStore = EnsureArg.IsNotNull(instanceStore, nameof(instanceStore)); - _partition = EnsureArg.IsNotNull(partition, nameof(partition)); - _identifiers = new Queue(EnsureArg.IsNotNull(options?.Values, nameof(options))); - } - - public ValueTask DisposeAsync() - => ValueTask.CompletedTask; - - public async IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) - { - foreach (DicomIdentifier identifier in _identifiers) - { - // Attempt to read the data - IReadOnlyList instances = await _instanceStore.GetInstanceIdentifierWithPropertiesAsync( - _partition, - identifier.StudyInstanceUid, - identifier.SeriesInstanceUid, - identifier.SopInstanceUid, - isInitialVersion: false, - cancellationToken); - - if (instances.Count == 0) - { - var args = new ReadFailureEventArgs( - identifier, - identifier.Type switch - { - ResourceType.Study => new StudyNotFoundException(), - ResourceType.Series => new SeriesNotFoundException(), - _ => new InstanceNotFoundException(), - }); - - ReadFailure?.Invoke(this, args); - yield return ReadResult.ForFailure(args); - } - else - { - foreach (InstanceMetadata read in instances) - yield return ReadResult.ForInstance(read); - } - } - } - - public bool TryDequeueBatch(int size, out ExportDataOptions batch) - { - EnsureArg.IsGt(size, 0, nameof(size)); - - if (_identifiers.Count == 0) - { - batch = default; - return false; - } - - var elements = new List(); - while (elements.Count < size && _identifiers.TryDequeue(out DicomIdentifier identifier)) - { - elements.Add(identifier); - } - - batch = CreateOptions(elements); - return true; - } - - private static ExportDataOptions CreateOptions(IReadOnlyCollection values) - => new ExportDataOptions(ExportSourceType.Identifiers, new IdentifierExportOptions { Values = values }); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Export/IdentifierExportSourceProvider.cs b/src/Microsoft.Health.Dicom.Core/Features/Export/IdentifierExportSourceProvider.cs deleted file mode 100644 index 767ba2b7ec..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Export/IdentifierExportSourceProvider.cs +++ /dev/null @@ -1,40 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Core.Features.Retrieve; -using Microsoft.Health.Dicom.Core.Models.Export; - -namespace Microsoft.Health.Dicom.Core.Features.Export; - -internal sealed class IdentifierExportSourceProvider : ExportSourceProvider, IExportSourceProvider -{ - public override ExportSourceType Type => ExportSourceType.Identifiers; - - private readonly IInstanceStore _instanceStore; - - public IdentifierExportSourceProvider(IInstanceStore instanceStore) - => _instanceStore = EnsureArg.IsNotNull(instanceStore, nameof(instanceStore)); - - [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "Callers will dipose of source.")] - protected override Task CreateAsync(IdentifierExportOptions options, Partition partition, CancellationToken cancellationToken = default) - => Task.FromResult(new IdentifierExportSource(_instanceStore, partition, options)); - - protected override Task ValidateAsync(IdentifierExportOptions options, CancellationToken cancellationToken = default) - { - List errors = options.Validate(new ValidationContext(this)).ToList(); - - return errors.Count > 0 - ? Task.FromException(new ValidationException(errors.First().ErrorMessage)) - : Task.CompletedTask; - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Export/ReadFailureEventArgs.cs b/src/Microsoft.Health.Dicom.Core/Features/Export/ReadFailureEventArgs.cs deleted file mode 100644 index 72f88cf876..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Export/ReadFailureEventArgs.cs +++ /dev/null @@ -1,42 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using EnsureThat; -using Microsoft.Health.Dicom.Core.Models.Common; - -namespace Microsoft.Health.Dicom.Core.Features.Export; - -/// -/// Provides data for the event . -/// -public sealed class ReadFailureEventArgs : EventArgs -{ - /// - /// Gets the identifier for the DICOM file(s) that failed to be read. - /// - /// An identifier representing a study, series, or SOP instance. - public DicomIdentifier Identifier { get; } - - /// - /// Gets the exception that caused the failure. - /// - /// An instance of the class. - public Exception Exception { get; } - - /// - /// Initializes a new instance of the class. - /// - /// An identifier for the DICOM file(s) that failed to be read. - /// The exception raised by the read failure. - /// - /// is . - /// - public ReadFailureEventArgs(DicomIdentifier identifier, Exception exception) - { - Identifier = identifier; - Exception = EnsureArg.IsNotNull(exception, nameof(exception)); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/AddExtendedQueryTagEntry.cs b/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/AddExtendedQueryTagEntry.cs deleted file mode 100644 index 69ddf59120..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/AddExtendedQueryTagEntry.cs +++ /dev/null @@ -1,43 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Globalization; - -namespace Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -/// -/// External representation of a extended query tag entry for add. -/// -public class AddExtendedQueryTagEntry : ExtendedQueryTagEntry, IValidatableObject -{ - /// - /// Level of this tag. Could be Study, Series or Instance. - /// - public QueryTagLevel? Level { get; set; } - - public IEnumerable Validate(ValidationContext validationContext) - { - string property; - if (string.IsNullOrWhiteSpace(Path)) - { - property = nameof(Path); - yield return new ValidationResult(string.Format(CultureInfo.CurrentCulture, DicomCoreResource.AddExtendedQueryTagEntryPropertyNotSpecified, property), new[] { property }); - } - - if (!Level.HasValue) - { - property = nameof(Level); - yield return new ValidationResult(string.Format(CultureInfo.CurrentCulture, DicomCoreResource.AddExtendedQueryTagEntryPropertyNotSpecified, property), new[] { property }); - } - else if (!Enum.IsDefined(Level.GetValueOrDefault())) - { - property = nameof(Level); - yield return new ValidationResult(string.Format(CultureInfo.CurrentCulture, DicomCoreResource.InvalidDicomTagLevel, Level), new[] { property }); - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/AddExtendedQueryTagHandler.cs b/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/AddExtendedQueryTagHandler.cs deleted file mode 100644 index 59861b07d5..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/AddExtendedQueryTagHandler.cs +++ /dev/null @@ -1,41 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using MediatR; -using Microsoft.Health.Core.Features.Security.Authorization; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Security; -using Microsoft.Health.Dicom.Core.Messages.ExtendedQueryTag; - -namespace Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -public class AddExtendedQueryTagHandler : BaseHandler, IRequestHandler -{ - private readonly IAddExtendedQueryTagService _addExtendedQueryTagService; - - public AddExtendedQueryTagHandler(IAuthorizationService authorizationService, IAddExtendedQueryTagService addExtendedQueryTagService) - : base(authorizationService) - { - EnsureArg.IsNotNull(addExtendedQueryTagService, nameof(addExtendedQueryTagService)); - _addExtendedQueryTagService = addExtendedQueryTagService; - } - - public async Task Handle(AddExtendedQueryTagRequest request, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(request, nameof(request)); - - if (await AuthorizationService.CheckAccess(DataActions.ManageExtendedQueryTags, cancellationToken) != DataActions.ManageExtendedQueryTags) - { - throw new UnauthorizedDicomActionException(DataActions.ManageExtendedQueryTags); - } - - return new AddExtendedQueryTagResponse( - await _addExtendedQueryTagService.AddExtendedQueryTagsAsync(request.ExtendedQueryTags, cancellationToken)); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/AddExtendedQueryTagService.cs b/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/AddExtendedQueryTagService.cs deleted file mode 100644 index c6c2dd0826..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/AddExtendedQueryTagService.cs +++ /dev/null @@ -1,98 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Operations; -using Microsoft.Health.Dicom.Core.Models.Operations; -using Microsoft.Health.Operations; - -namespace Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -public class AddExtendedQueryTagService : IAddExtendedQueryTagService -{ - private readonly IExtendedQueryTagStore _extendedQueryTagStore; - private readonly IGuidFactory _guidFactory; - private readonly IDicomOperationsClient _client; - private readonly IExtendedQueryTagEntryValidator _extendedQueryTagEntryValidator; - private readonly int _maxAllowedCount; - - private static readonly OperationQueryCondition ReindexQuery = new OperationQueryCondition - { - Operations = new DicomOperation[] { DicomOperation.Reindex }, - Statuses = new OperationStatus[] - { - OperationStatus.NotStarted, - OperationStatus.Running, - } - }; - - public AddExtendedQueryTagService( - IExtendedQueryTagStore extendedQueryTagStore, - IGuidFactory guidFactory, - IDicomOperationsClient client, - IExtendedQueryTagEntryValidator extendedQueryTagEntryValidator, - IOptions extendedQueryTagConfiguration) - { - EnsureArg.IsNotNull(extendedQueryTagStore, nameof(extendedQueryTagStore)); - EnsureArg.IsNotNull(guidFactory, nameof(guidFactory)); - EnsureArg.IsNotNull(client, nameof(client)); - EnsureArg.IsNotNull(extendedQueryTagEntryValidator, nameof(extendedQueryTagEntryValidator)); - EnsureArg.IsNotNull(extendedQueryTagConfiguration?.Value, nameof(extendedQueryTagConfiguration)); - - _extendedQueryTagStore = extendedQueryTagStore; - _guidFactory = guidFactory; - _client = client; - _extendedQueryTagEntryValidator = extendedQueryTagEntryValidator; - _maxAllowedCount = extendedQueryTagConfiguration.Value.MaxAllowedCount; - } - - public async Task AddExtendedQueryTagsAsync( - IEnumerable extendedQueryTags, - CancellationToken cancellationToken = default) - { - // Check if any extended query tag operation is ongoing - OperationReference activeReindex = await _client - .FindOperationsAsync(ReindexQuery, cancellationToken) - .FirstOrDefaultAsync(cancellationToken); - - if (activeReindex != null) - throw new ExistingOperationException(activeReindex, "re-index"); - - _extendedQueryTagEntryValidator.ValidateExtendedQueryTags(extendedQueryTags); - var normalized = extendedQueryTags - .Select(item => item.Normalize()) - .ToList(); - - // Add the extended query tags to the DB - IReadOnlyList added = await _extendedQueryTagStore.AddExtendedQueryTagsAsync( - normalized, - _maxAllowedCount, - ready: false, - cancellationToken: cancellationToken); - - // Start re-indexing - var tagKeys = added.Select(x => x.Key).ToList(); - OperationReference operation = await _client.StartReindexingInstancesAsync(_guidFactory.Create(), tagKeys, cancellationToken); - - // Associate the tags to the operation and confirm their processing - IReadOnlyList confirmedTags = await _extendedQueryTagStore.AssignReindexingOperationAsync( - tagKeys, - operation.Id, - returnIfCompleted: true, - cancellationToken: cancellationToken); - - return confirmedTags.Count > 0 ? operation : throw new ExtendedQueryTagsAlreadyExistsException(); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/DeleteExtendedQueryTagHandler.cs b/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/DeleteExtendedQueryTagHandler.cs deleted file mode 100644 index 8a22ed1fb6..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/DeleteExtendedQueryTagHandler.cs +++ /dev/null @@ -1,41 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using MediatR; -using Microsoft.Health.Core.Features.Security.Authorization; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Security; -using Microsoft.Health.Dicom.Core.Messages.ExtendedQueryTag; - -namespace Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -public class DeleteExtendedQueryTagHandler : BaseHandler, IRequestHandler -{ - private readonly IDeleteExtendedQueryTagService _deleteExtendedQueryTagService; - - public DeleteExtendedQueryTagHandler(IAuthorizationService authorizationService, IDeleteExtendedQueryTagService deleteExtendedQueryTagService) - : base(authorizationService) - { - EnsureArg.IsNotNull(deleteExtendedQueryTagService, nameof(deleteExtendedQueryTagService)); - _deleteExtendedQueryTagService = deleteExtendedQueryTagService; - } - - public async Task Handle(DeleteExtendedQueryTagRequest request, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(request, nameof(request)); - - if (await AuthorizationService.CheckAccess(DataActions.ManageExtendedQueryTags, cancellationToken) != DataActions.ManageExtendedQueryTags) - { - throw new UnauthorizedDicomActionException(DataActions.ManageExtendedQueryTags); - } - - await _deleteExtendedQueryTagService.DeleteExtendedQueryTagAsync(request.TagPath, cancellationToken); - return new DeleteExtendedQueryTagResponse(); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/DeleteExtendedQueryTagService.cs b/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/DeleteExtendedQueryTagService.cs deleted file mode 100644 index f013ab0783..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/DeleteExtendedQueryTagService.cs +++ /dev/null @@ -1,44 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Globalization; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Common; - -namespace Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -public class DeleteExtendedQueryTagService : IDeleteExtendedQueryTagService -{ - private readonly IExtendedQueryTagStore _extendedQueryTagStore; - private readonly IDicomTagParser _dicomTagParser; - - public DeleteExtendedQueryTagService(IExtendedQueryTagStore extendedQueryTagStore, IDicomTagParser dicomTagParser) - { - EnsureArg.IsNotNull(extendedQueryTagStore, nameof(extendedQueryTagStore)); - EnsureArg.IsNotNull(dicomTagParser, nameof(dicomTagParser)); - - _extendedQueryTagStore = extendedQueryTagStore; - _dicomTagParser = dicomTagParser; - } - - public async Task DeleteExtendedQueryTagAsync(string tagPath, CancellationToken cancellationToken) - { - DicomTag[] tags; - if (!_dicomTagParser.TryParse(tagPath, out tags)) - { - throw new InvalidExtendedQueryTagPathException( - string.Format(CultureInfo.InvariantCulture, DicomCoreResource.InvalidExtendedQueryTag, tagPath ?? string.Empty)); - } - - string normalizedPath = tags[0].GetPath(); - ExtendedQueryTagStoreEntry extendedQueryTagEntry = await _extendedQueryTagStore.GetExtendedQueryTagAsync(normalizedPath, cancellationToken); - await _extendedQueryTagStore.DeleteExtendedQueryTagAsync(normalizedPath, extendedQueryTagEntry.VR, cancellationToken); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/ExtendedQueryTagEntry.cs b/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/ExtendedQueryTagEntry.cs deleted file mode 100644 index 9a50c5828d..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/ExtendedQueryTagEntry.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -/// -/// Representation of a extended query tag entry. -/// -public abstract class ExtendedQueryTagEntry : QueryTagEntry -{ - /// - /// Identification code of private tag implementer. - /// - public string PrivateCreator { get; set; } - - public override string ToString() - { - return $"Path: {Path}, VR:{VR}, PrivateCreator:{PrivateCreator}"; - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/ExtendedQueryTagEntryValidator.cs b/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/ExtendedQueryTagEntryValidator.cs deleted file mode 100644 index df4a071225..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/ExtendedQueryTagEntryValidator.cs +++ /dev/null @@ -1,218 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Globalization; -using System.Linq; -using EnsureThat; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Query; -using Microsoft.Health.Dicom.Core.Features.Validation; - -namespace Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -public class ExtendedQueryTagEntryValidator : IExtendedQueryTagEntryValidator -{ - private readonly IDicomTagParser _dicomTagParser; - - public ExtendedQueryTagEntryValidator(IDicomTagParser dicomTagParser) - { - EnsureArg.IsNotNull(dicomTagParser, nameof(dicomTagParser)); - _dicomTagParser = dicomTagParser; - } - - /* - * Unsupported VRCodes: - * LT(Long Text), OB (Other Byte), OD (Other Double), OF(Other Float), OL (Other Long), OV(other Very long), OW (other Word), ST(Short Text, SV (Signed Very long) - * UC (Unlimited Characters), UN (Unknown), UR (URI), UT (Unlimited Text), UV (Unsigned Very long) - * Note: we dont' find definition for UR, UV and SV in DICOM standard (http://dicom.nema.org/dicom/2013/output/chtml/part05/sect_6.2.html) - */ - public static ImmutableHashSet SupportedVRCodes { get; } = ImmutableHashSet.Create( - DicomVRCode.AE, - DicomVRCode.AS, - DicomVRCode.CS, - DicomVRCode.DA, - DicomVRCode.DT, - DicomVRCode.FD, - DicomVRCode.FL, - DicomVRCode.IS, - DicomVRCode.LO, - DicomVRCode.PN, - DicomVRCode.SH, - DicomVRCode.SL, - DicomVRCode.SS, - DicomVRCode.TM, - DicomVRCode.UI, - DicomVRCode.UL, - DicomVRCode.US); - - public void ValidateExtendedQueryTags(IEnumerable extendedQueryTagEntries) - { - EnsureArg.IsNotNull(extendedQueryTagEntries, nameof(extendedQueryTagEntries)); - - var pathSet = new HashSet(StringComparer.OrdinalIgnoreCase); - foreach (AddExtendedQueryTagEntry tagEntry in extendedQueryTagEntries) - { - ValidateExtendedQueryTagEntry(tagEntry); - - // don't allow duplicated path - if (pathSet.Contains(tagEntry.Path)) - { - throw new ExtendedQueryTagEntryValidationException( - string.Format(CultureInfo.InvariantCulture, DicomCoreResource.DuplicateExtendedQueryTag, tagEntry.Path)); - } - - pathSet.Add(tagEntry.Path); - } - - if (pathSet.Count == 0) - { - throw new ExtendedQueryTagEntryValidationException(DicomCoreResource.MissingExtendedQueryTag); - } - } - - /// - /// Validate extended query tag entry. - /// - /// the tag entry. - private void ValidateExtendedQueryTagEntry(AddExtendedQueryTagEntry tagEntry) - { - DicomTag tag = ParseTag(tagEntry.Path); - - // cannot be any tag we already support - if (QueryLimit.CoreFilterTags.Contains(tag)) - { - throw new ExtendedQueryTagEntryValidationException( - string.Format(CultureInfo.InvariantCulture, DicomCoreResource.QueryTagAlreadySupported, tagEntry.Path)); - } - - if (tagEntry.Level == null) - { - throw new ExtendedQueryTagEntryValidationException( - string.Format(CultureInfo.InvariantCulture, DicomCoreResource.MissingLevel, tagEntry.Path)); - } - - ValidatePrivateCreator(tag, tagEntry.PrivateCreator, tagEntry.Path); - - ValidateVRCode(tag, tagEntry.VR, tagEntry.Path); - } - - private static void ValidateVRCode(DicomTag tag, string vrCode, string tagPath) - { - DicomVR dicomVR = string.IsNullOrWhiteSpace(vrCode) ? null : ParseVRCode(vrCode, tagPath); - - if (tag.DictionaryEntry != DicomDictionary.UnknownTag) - { - // if VR is specified for knownTag, validate - if (dicomVR != null) - { - if (!tag.DictionaryEntry.ValueRepresentations.Contains(dicomVR)) - { - // not a valid VR - throw new ExtendedQueryTagEntryValidationException( - string.Format(CultureInfo.InvariantCulture, DicomCoreResource.UnsupportedVRCodeOnTag, vrCode, tagPath, tag.GetDefaultVR())); - } - } - else - { - // otherwise, get default one - dicomVR = tag.GetDefaultVR(); - } - } - else - { - // for unknown tag, vrCode is required - if (dicomVR == null) - { - throw new ExtendedQueryTagEntryValidationException( - string.Format(CultureInfo.InvariantCulture, DicomCoreResource.MissingVRCode, tagPath)); - } - } - - EnsureVRIsSupported(dicomVR, tagPath); - } - - private static void ValidatePrivateCreator(DicomTag tag, string privateCreator, string tagPath) - { - if (!tag.IsPrivate) - { - // Standard tags should not have private creator. - if (!string.IsNullOrWhiteSpace(privateCreator)) - { - throw new ExtendedQueryTagEntryValidationException( - string.Format(CultureInfo.InvariantCulture, DicomCoreResource.PrivateCreatorNotEmpty, tagPath)); - } - return; - } - - // PrivateCreator Tag should not have privateCreator. - if (tag.DictionaryEntry == DicomDictionary.PrivateCreatorTag) - { - if (!string.IsNullOrWhiteSpace(privateCreator)) - { - throw new ExtendedQueryTagEntryValidationException( - string.Format(CultureInfo.InvariantCulture, DicomCoreResource.PrivateCreatorNotEmptyForPrivateIdentificationCode, tagPath)); - } - return; - } - - // Private tag except PrivateCreator requires privateCreator - if (string.IsNullOrWhiteSpace(privateCreator)) - { - throw new ExtendedQueryTagEntryValidationException( - string.Format(CultureInfo.InvariantCulture, DicomCoreResource.MissingPrivateCreator, tagPath)); - } - - try - { - LongStringValidation.Validate(privateCreator, nameof(privateCreator)); - } - catch (ElementValidationException ex) - { - throw new ExtendedQueryTagEntryValidationException( - string.Format(CultureInfo.InvariantCulture, DicomCoreResource.PrivateCreatorNotValidLO, tagPath), ex); - } - - } - - private static DicomVR ParseVRCode(string vrCode, string tagPath) - { - try - { - // DicomVR.Parse only accept upper case VR code. - return DicomVR.Parse(vrCode.ToUpper(CultureInfo.InvariantCulture)); - } - catch (DicomDataException ex) - { - throw new ExtendedQueryTagEntryValidationException( - string.Format(CultureInfo.InvariantCulture, DicomCoreResource.InvalidVRCode, vrCode, tagPath), ex); - } - } - - private DicomTag ParseTag(string path) - { - if (!_dicomTagParser.TryParse(path, out DicomTag[] result)) - { - throw new ExtendedQueryTagEntryValidationException( - string.Format(CultureInfo.InvariantCulture, DicomCoreResource.InvalidExtendedQueryTag, path)); - } - - return result[0]; - } - - private static void EnsureVRIsSupported(DicomVR vr, string tagPath) - { - if (!SupportedVRCodes.Contains(vr.Code)) - { - throw new ExtendedQueryTagEntryValidationException( - string.Format(CultureInfo.InvariantCulture, DicomCoreResource.UnsupportedVRCode, vr.Code, tagPath)); - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/ExtendedQueryTagError.cs b/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/ExtendedQueryTagError.cs deleted file mode 100644 index 1171446f90..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/ExtendedQueryTagError.cs +++ /dev/null @@ -1,43 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using EnsureThat; - -namespace Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -/// -/// Represents each Extended Query Tag Error that will be surfaced to the user. -/// -public class ExtendedQueryTagError -{ - public ExtendedQueryTagError( - DateTime createdTime, - string studyInstanceUid, - string seriesInstanceUid, - string sopInstanceUid, - string errorMessage, - string partitionName = default) - { - StudyInstanceUid = EnsureArg.IsNotNullOrWhiteSpace(studyInstanceUid); - SeriesInstanceUid = EnsureArg.IsNotNullOrWhiteSpace(seriesInstanceUid); - SopInstanceUid = EnsureArg.IsNotNullOrWhiteSpace(sopInstanceUid); - CreatedTime = createdTime; - ErrorMessage = EnsureArg.IsNotNullOrWhiteSpace(errorMessage); - PartitionName = partitionName; - } - - public string PartitionName { get; } - - public string StudyInstanceUid { get; } - - public string SeriesInstanceUid { get; } - - public string SopInstanceUid { get; } - - public DateTime CreatedTime { get; } - - public string ErrorMessage { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/ExtendedQueryTagErrorReference.cs b/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/ExtendedQueryTagErrorReference.cs deleted file mode 100644 index efaabbd272..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/ExtendedQueryTagErrorReference.cs +++ /dev/null @@ -1,44 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using EnsureThat; - -namespace Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -/// -/// Represents a reference to a one or more extended query tag errors. -/// -public class ExtendedQueryTagErrorReference -{ - /// - /// Initializes a new instance of the class. - /// - /// The number of errors. - /// The resource URL for the operation. - /// - /// is . - /// - /// - /// is less than 1. - /// - public ExtendedQueryTagErrorReference(int count, Uri href) - { - Count = EnsureArg.IsGte(count, 0, nameof(count)); - Href = EnsureArg.IsNotNull(href, nameof(href)); - } - - /// - /// Gets the number of errors. - /// - /// The positive number of errors found at the . - public int Count { get; } - - /// - /// Gets the resource reference for the errors. - /// - /// The unique resource URL for the extended query tag errors. - public Uri Href { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/ExtendedQueryTagErrorsService.cs b/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/ExtendedQueryTagErrorsService.cs deleted file mode 100644 index f2b9b71b91..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/ExtendedQueryTagErrorsService.cs +++ /dev/null @@ -1,55 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Globalization; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Validation; -using Microsoft.Health.Dicom.Core.Messages.ExtendedQueryTag; - -namespace Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -public class ExtendedQueryTagErrorsService : IExtendedQueryTagErrorsService -{ - private readonly IExtendedQueryTagErrorStore _extendedQueryTagErrorStore; - private readonly IDicomTagParser _dicomTagParser; - - public ExtendedQueryTagErrorsService(IExtendedQueryTagErrorStore extendedQueryTagStore, IDicomTagParser dicomTagParser) - { - _extendedQueryTagErrorStore = EnsureArg.IsNotNull(extendedQueryTagStore, nameof(extendedQueryTagStore)); - _dicomTagParser = EnsureArg.IsNotNull(dicomTagParser, nameof(dicomTagParser)); - } - - public async Task GetExtendedQueryTagErrorsAsync(string tagPath, int limit, long offset, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(tagPath, nameof(tagPath)); - string numericalTagPath = _dicomTagParser.TryParse(tagPath, out DicomTag[] tags) - ? tags[0].GetPath() - : throw new InvalidExtendedQueryTagPathException(string.Format(CultureInfo.CurrentCulture, DicomCoreResource.InvalidExtendedQueryTag, tagPath ?? string.Empty)); - - IReadOnlyList extendedQueryTagErrors = await _extendedQueryTagErrorStore.GetExtendedQueryTagErrorsAsync( - numericalTagPath, - limit, - offset, - cancellationToken); - return new GetExtendedQueryTagErrorsResponse(extendedQueryTagErrors); - } - - public Task AddExtendedQueryTagErrorAsync(int tagKey, ValidationErrorCode errorCode, long watermark, CancellationToken cancellationToken) - { - EnsureArg.EnumIsDefined(errorCode, nameof(errorCode)); - return _extendedQueryTagErrorStore.AddExtendedQueryTagErrorAsync( - tagKey, - errorCode, - watermark, - cancellationToken); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/ExtendedQueryTagStatus.cs b/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/ExtendedQueryTagStatus.cs deleted file mode 100644 index a47b5974b3..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/ExtendedQueryTagStatus.cs +++ /dev/null @@ -1,27 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -/// -/// Status of extended query tag. -/// -public enum ExtendedQueryTagStatus -{ - /// - /// The extended query tag is being added. - /// - Adding = 0, - - /// - /// The extended query tag has been added to system. - /// - Ready = 1, - - /// - /// The extended query tag is being deleted. - /// - Deleting = 2, -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/ExtendedQueryTagStoreEntry.cs b/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/ExtendedQueryTagStoreEntry.cs deleted file mode 100644 index 58a65e78be..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/ExtendedQueryTagStoreEntry.cs +++ /dev/null @@ -1,51 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; - -namespace Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -/// -/// Represent each extended query tag entry has retrieved from the store. -/// -public class ExtendedQueryTagStoreEntry : ExtendedQueryTagEntry -{ - public ExtendedQueryTagStoreEntry(int key, string path, string vr, string privateCreator, QueryTagLevel level, ExtendedQueryTagStatus status, QueryStatus queryStatus, int errorCount) - { - Key = key; - Path = EnsureArg.IsNotNullOrWhiteSpace(path); - VR = EnsureArg.IsNotNullOrWhiteSpace(vr); - PrivateCreator = privateCreator; - Level = EnsureArg.EnumIsDefined(level); - Status = EnsureArg.EnumIsDefined(status); - QueryStatus = EnsureArg.EnumIsDefined(queryStatus); - ErrorCount = EnsureArg.IsGte(errorCount, 0, nameof(errorCount)); - } - - /// - /// Key of this extended query tag entry. - /// - public int Key { get; } - - /// - /// Status of this tag. - /// - public ExtendedQueryTagStatus Status { get; } - - /// - /// Level of this tag. Could be Study, Series or Instance. - /// - public QueryTagLevel Level { get; } - - /// - /// Query status of this tag. - /// - public QueryStatus QueryStatus { get; } - - /// - /// Error count on this tag. - /// - public int ErrorCount { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/ExtendedQueryTagStoreJoinEntry.cs b/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/ExtendedQueryTagStoreJoinEntry.cs deleted file mode 100644 index c3b729081b..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/ExtendedQueryTagStoreJoinEntry.cs +++ /dev/null @@ -1,76 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using EnsureThat; -using Microsoft.Health.Dicom.Core.Features.Routing; -using Microsoft.Health.Operations; - -namespace Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -/// -/// Represent an extended query tag entry has retrieved from the store that has been -/// joined with its corresponding optional operation. -/// -public class ExtendedQueryTagStoreJoinEntry : ExtendedQueryTagStoreEntry -{ - public ExtendedQueryTagStoreJoinEntry(ExtendedQueryTagStoreEntry storeEntry, Guid? operationId = null) - : base( - EnsureArg.IsNotNull(storeEntry, nameof(storeEntry)).Key, - storeEntry.Path, - storeEntry.VR, - storeEntry.PrivateCreator, - storeEntry.Level, - storeEntry.Status, - storeEntry.QueryStatus, - storeEntry.ErrorCount) - { - OperationId = operationId; - } - - public ExtendedQueryTagStoreJoinEntry( - int key, - string path, - string vr, - string privateCreator, - QueryTagLevel level, - ExtendedQueryTagStatus status, - QueryStatus queryStatus, - int errorCount, - Guid? operationId = null) - : base(key, path, vr, privateCreator, level, status, queryStatus, errorCount) - { - OperationId = operationId; - } - - /// - /// The optional ID for the long-running operation acted upon the tag. - /// - public Guid? OperationId { get; } - - /// - /// Convert to . - /// - /// An optional for resolving resource paths. - /// The extended query tag entry. - public GetExtendedQueryTagEntry ToGetExtendedQueryTagEntry(IUrlResolver resolver = null) - { - return new GetExtendedQueryTagEntry - { - Path = Path, - VR = VR, - PrivateCreator = PrivateCreator, - Level = Level, - Status = Status, - Errors = ErrorCount > 0 && resolver != null - ? new ExtendedQueryTagErrorReference(ErrorCount, resolver.ResolveQueryTagErrorsUri(Path)) - : null, - Operation = OperationId.HasValue && resolver != null - ? new OperationReference(OperationId.GetValueOrDefault(), resolver.ResolveOperationStatusUri(OperationId.GetValueOrDefault())) - : null, - QueryStatus = QueryStatus - }; - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/GetExtendedQueryTagEntry.cs b/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/GetExtendedQueryTagEntry.cs deleted file mode 100644 index f5a77d4fb0..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/GetExtendedQueryTagEntry.cs +++ /dev/null @@ -1,41 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Microsoft.Health.Operations; - -namespace Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -/// -/// External representation of a extended query tag entry for get. -/// -public class GetExtendedQueryTagEntry : ExtendedQueryTagEntry -{ - /// - /// Status of this tag. Represents the current state the tag is in. - /// - public ExtendedQueryTagStatus Status { get; set; } - - /// - /// Level of this tag. Could be Study, Series or Instance. - /// - public QueryTagLevel Level { get; set; } - - /// - /// Optional errors associated with the query tag. - /// - public ExtendedQueryTagErrorReference Errors { get; set; } - - /// - /// Optional reference to the operation acted upon the act. - /// - public OperationReference Operation { get; set; } - - public QueryStatus QueryStatus { get; set; } - - public override string ToString() - { - return $"Path: {Path}, VR:{VR}, PrivateCreator:{PrivateCreator}, Level:{Level}, Status:{Status}, Errors: {Errors?.Count ?? 0}, OperationId: {Operation?.Id}, QueryStatus: {QueryStatus}"; - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/GetExtendedQueryTagErrorsHandler.cs b/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/GetExtendedQueryTagErrorsHandler.cs deleted file mode 100644 index e1bf3431d1..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/GetExtendedQueryTagErrorsHandler.cs +++ /dev/null @@ -1,39 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using MediatR; -using Microsoft.Health.Core.Features.Security.Authorization; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Security; -using Microsoft.Health.Dicom.Core.Messages.ExtendedQueryTag; - -namespace Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -public class GetExtendedQueryTagErrorsHandler : BaseHandler, IRequestHandler -{ - private readonly IExtendedQueryTagErrorsService _getExtendedQueryTagErrorsService; - - public GetExtendedQueryTagErrorsHandler(IAuthorizationService authorizationService, IExtendedQueryTagErrorsService getExtendedQueryTagErrorsService) - : base(authorizationService) - { - _getExtendedQueryTagErrorsService = EnsureArg.IsNotNull(getExtendedQueryTagErrorsService, nameof(getExtendedQueryTagErrorsService)); - } - - public async Task Handle(GetExtendedQueryTagErrorsRequest request, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(request, nameof(request)); - - if (await AuthorizationService.CheckAccess(DataActions.Read, cancellationToken) != DataActions.Read) - { - throw new UnauthorizedDicomActionException(DataActions.Read); - } - - return await _getExtendedQueryTagErrorsService.GetExtendedQueryTagErrorsAsync(request.Path, request.Limit, request.Offset, cancellationToken); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/GetExtendedQueryTagHandler.cs b/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/GetExtendedQueryTagHandler.cs deleted file mode 100644 index 1c931aed02..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/GetExtendedQueryTagHandler.cs +++ /dev/null @@ -1,40 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using MediatR; -using Microsoft.Health.Core.Features.Security.Authorization; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Security; -using Microsoft.Health.Dicom.Core.Messages.ExtendedQueryTag; - -namespace Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -public class GetExtendedQueryTagHandler : BaseHandler, IRequestHandler -{ - private readonly IGetExtendedQueryTagsService _getExtendedQueryTagsService; - - public GetExtendedQueryTagHandler(IAuthorizationService authorizationService, IGetExtendedQueryTagsService getExtendedQueryTagsService) - : base(authorizationService) - { - EnsureArg.IsNotNull(getExtendedQueryTagsService, nameof(getExtendedQueryTagsService)); - _getExtendedQueryTagsService = getExtendedQueryTagsService; - } - - public async Task Handle(GetExtendedQueryTagRequest request, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(request, nameof(request)); - - if (await AuthorizationService.CheckAccess(DataActions.Read, cancellationToken) != DataActions.Read) - { - throw new UnauthorizedDicomActionException(DataActions.Read); - } - - return await _getExtendedQueryTagsService.GetExtendedQueryTagAsync(request.ExtendedQueryTagPath, cancellationToken); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/GetExtendedQueryTagsHandler.cs b/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/GetExtendedQueryTagsHandler.cs deleted file mode 100644 index 44bce628f4..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/GetExtendedQueryTagsHandler.cs +++ /dev/null @@ -1,40 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using MediatR; -using Microsoft.Health.Core.Features.Security.Authorization; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Security; -using Microsoft.Health.Dicom.Core.Messages.ExtendedQueryTag; - -namespace Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -public class GetExtendedQueryTagsHandler : BaseHandler, IRequestHandler -{ - private readonly IGetExtendedQueryTagsService _getExtendedQueryTagsService; - - public GetExtendedQueryTagsHandler(IAuthorizationService authorizationService, IGetExtendedQueryTagsService getExtendedQueryTagsService) - : base(authorizationService) - { - EnsureArg.IsNotNull(getExtendedQueryTagsService, nameof(getExtendedQueryTagsService)); - _getExtendedQueryTagsService = getExtendedQueryTagsService; - } - - public async Task Handle(GetExtendedQueryTagsRequest request, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(request, nameof(request)); - - if (await AuthorizationService.CheckAccess(DataActions.Read, cancellationToken) != DataActions.Read) - { - throw new UnauthorizedDicomActionException(DataActions.Read); - } - - return await _getExtendedQueryTagsService.GetExtendedQueryTagsAsync(request.Limit, request.Offset, cancellationToken); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/GetExtendedQueryTagsService.cs b/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/GetExtendedQueryTagsService.cs deleted file mode 100644 index 9ea5da38d5..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/GetExtendedQueryTagsService.cs +++ /dev/null @@ -1,65 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Routing; -using Microsoft.Health.Dicom.Core.Messages.ExtendedQueryTag; - -namespace Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -public class GetExtendedQueryTagsService : IGetExtendedQueryTagsService -{ - private readonly IExtendedQueryTagStore _extendedQueryTagStore; - private readonly IDicomTagParser _dicomTagParser; - private readonly IUrlResolver _urlResolver; - - public GetExtendedQueryTagsService( - IExtendedQueryTagStore extendedQueryTagStore, - IDicomTagParser dicomTagParser, - IUrlResolver urlResolver) - { - _extendedQueryTagStore = EnsureArg.IsNotNull(extendedQueryTagStore, nameof(extendedQueryTagStore)); - _dicomTagParser = EnsureArg.IsNotNull(dicomTagParser, nameof(dicomTagParser)); - _urlResolver = EnsureArg.IsNotNull(urlResolver, nameof(urlResolver)); - } - - public async Task GetExtendedQueryTagAsync(string tagPath, CancellationToken cancellationToken = default) - { - DicomTag[] tags; - string numericalTagPath; - if (_dicomTagParser.TryParse(tagPath, out tags)) - { - if (tags.Length > 1) - { - throw new NotImplementedException(DicomCoreResource.SequentialDicomTagsNotSupported); - } - - numericalTagPath = tags[0].GetPath(); - } - else - { - throw new InvalidExtendedQueryTagPathException(string.Format(CultureInfo.CurrentCulture, DicomCoreResource.InvalidExtendedQueryTag, tagPath ?? string.Empty)); - } - - ExtendedQueryTagStoreJoinEntry extendedQueryTag = await _extendedQueryTagStore.GetExtendedQueryTagAsync(numericalTagPath, cancellationToken); - return new GetExtendedQueryTagResponse(extendedQueryTag.ToGetExtendedQueryTagEntry(_urlResolver)); - } - - public async Task GetExtendedQueryTagsAsync(int limit, long offset = 0, CancellationToken cancellationToken = default) - { - IReadOnlyList extendedQueryTags = await _extendedQueryTagStore.GetExtendedQueryTagsAsync(limit, offset, cancellationToken); - return new GetExtendedQueryTagsResponse(extendedQueryTags.Select(x => x.ToGetExtendedQueryTagEntry(_urlResolver))); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/IAddExtendedQueryTagService.cs b/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/IAddExtendedQueryTagService.cs deleted file mode 100644 index bd2244c12c..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/IAddExtendedQueryTagService.cs +++ /dev/null @@ -1,27 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Operations; - -namespace Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -public interface IAddExtendedQueryTagService -{ - /// - /// Add Extended Query Tags. - /// - /// The extended query tags. - /// The cancellation token. - /// The response. - /// There is already an ongoing re-index operation. - /// - /// One or more values in has already been indexed. - /// - public Task AddExtendedQueryTagsAsync(IEnumerable extendedQueryTags, CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/IDeleteExtendedQueryTagService.cs b/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/IDeleteExtendedQueryTagService.cs deleted file mode 100644 index 775773d99e..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/IDeleteExtendedQueryTagService.cs +++ /dev/null @@ -1,20 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -public interface IDeleteExtendedQueryTagService -{ - /// - /// Delete extended query tag. - /// - /// The extended query tag path. - /// The cancellation token. - /// The task. - public Task DeleteExtendedQueryTagAsync(string tagPath, CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/IExtendedQueryTagEntryValidator.cs b/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/IExtendedQueryTagEntryValidator.cs deleted file mode 100644 index d2359f7e4f..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/IExtendedQueryTagEntryValidator.cs +++ /dev/null @@ -1,20 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; - -namespace Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -/// -/// Validate if given extended query tag entries are valid -/// -public interface IExtendedQueryTagEntryValidator -{ - /// - /// Validate if given extended query tag entries are valid. - /// - /// The extended query tag entries - void ValidateExtendedQueryTags(IEnumerable extendedQueryTagEntries); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/IExtendedQueryTagErrorStore.cs b/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/IExtendedQueryTagErrorStore.cs deleted file mode 100644 index 00d29907eb..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/IExtendedQueryTagErrorStore.cs +++ /dev/null @@ -1,47 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Health.Dicom.Core.Features.Validation; - -namespace Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -/// -/// The error store saving extended query tag errors. -/// -public interface IExtendedQueryTagErrorStore -{ - /// - /// Get extended query tags errors by tag path. - /// - /// The tag path. - /// The maximum number of results to retrieve. - /// The offset from which to retrieve paginated results. - /// The cancellation token. - /// A list of Extended Query Tag Errors. - /// - /// is less than 1 - /// -or- - /// is less than 0. - /// - Task> GetExtendedQueryTagErrorsAsync(string tagPath, int limit, long offset = 0, CancellationToken cancellationToken = default); - - /// - /// Asynchronously adds an error for a specified Extended Query Tag. - /// - /// TagKey of the extended query tag to which an error will be added. - /// Validation error code. - /// Watermark. - /// The cancellation token. - /// A task. - Task AddExtendedQueryTagErrorAsync( - int tagKey, - ValidationErrorCode errorCode, - long watermark, - CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/IExtendedQueryTagErrorsService.cs b/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/IExtendedQueryTagErrorsService.cs deleted file mode 100644 index de15ce7c89..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/IExtendedQueryTagErrorsService.cs +++ /dev/null @@ -1,46 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Health.Dicom.Core.Features.Validation; -using Microsoft.Health.Dicom.Core.Messages.ExtendedQueryTag; - -namespace Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -/// -/// The service for interacting with extended query tag error store. -/// -public interface IExtendedQueryTagErrorsService -{ - /// - /// Asynchronously adds an error for a specified Extended Query Tag. - /// - /// TagKey of the extended query tag to which an error will be added. - /// The validation error code. - /// Watermark. - /// The cancellation token. - Task AddExtendedQueryTagErrorAsync( - int tagKey, - ValidationErrorCode errorCode, - long watermark, - CancellationToken cancellationToken = default); - - /// - /// Asynchronously gets errors for a specified Extended Query Tag. - /// - /// Path to the extended query tag that is requested. - /// The maximum number of results to retrieve. - /// The offset from which to retrieve paginated results. - /// The cancellation token. - /// The response. - /// - /// is less than 1 - /// -or- - /// is less than 0. - /// - Task GetExtendedQueryTagErrorsAsync(string tagPath, int limit, long offset = 0, CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/IExtendedQueryTagStore.cs b/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/IExtendedQueryTagStore.cs deleted file mode 100644 index 8358b954a3..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/IExtendedQueryTagStore.cs +++ /dev/null @@ -1,146 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -/// -/// The store saving extended query tags. -/// -public interface IExtendedQueryTagStore -{ - /// - /// Asynchronously adds the extended query tags into the store if they are not present. - /// - /// The extended query tag entries. - /// The max allowed count. - /// Optionally indicates whether the have been fully indexed. - /// - /// The token to monitor for cancellation requests. The default value is . - /// - /// - /// A task representing the asynchronous add operation. The value of its - /// property contains the added extended query tags. - /// - /// The was canceled. - Task> AddExtendedQueryTagsAsync( - IReadOnlyCollection extendedQueryTagEntries, - int maxAllowedCount, - bool ready = false, - CancellationToken cancellationToken = default); - - /// - /// Get the stored extended query tag from ExtendedQueryTagStore by its path. - /// - /// Path associated with requested extended query tag formatted as it is stored internally. - /// The cancellation token. - /// - /// A task representing the asynchronous get operation. The value of its - /// property contains the tag's information as found in storage. - /// - Task GetExtendedQueryTagAsync(string tagPath, CancellationToken cancellationToken = default); - - /// - /// Get stored extended query tags from ExtendedQueryTagStore, if provided, by tagPath. - /// - /// The maximum number of results to retrieve. - /// The offset from which to retrieve paginated results. - /// The cancellation token. - /// - /// A task representing the asynchronous get operation. The value of its - /// property contains a list of the tags' information as found in storage. - /// - /// - /// is less than 1 - /// -or- - /// is less than 0. - /// - Task> GetExtendedQueryTagsAsync(int limit, long offset = 0, CancellationToken cancellationToken = default); - - /// - /// Asynchronously gets extended query tags by keys. - /// - /// The tag keys. - /// The cancellation token. - /// The task. - Task> GetExtendedQueryTagsAsync(IReadOnlyCollection queryTagKeys, CancellationToken cancellationToken = default); - - /// - /// Update QueryStatus of extended query tag. - /// - /// The tag path. - /// The query status. - /// The cancellation token - /// The updated extended query tag. - Task UpdateQueryStatusAsync(string tagPath, QueryStatus queryStatus, CancellationToken cancellationToken = default); - - /// - /// Asynchronously gets extended query tags assigned to the . - /// - /// The unique ID for the re-indexing operation. - /// - /// The token to monitor for cancellation requests. The default value is . - /// - /// - /// A task representing the operation. - /// The value of its property contains the set of query tags assigned - /// to the . - /// - /// The was canceled. - Task> GetExtendedQueryTagsAsync(Guid operationId, CancellationToken cancellationToken = default); - - /// - /// Asynchronously deletes extended query tag. - /// - /// The tag path. - /// The VR code. - /// The cancellation token. - /// The task. - Task DeleteExtendedQueryTagAsync(string tagPath, string vr, CancellationToken cancellationToken = default); - - /// - /// Asynchronously assigns the given to the given tag keys. - /// - /// The keys for the extended query tags. - /// The unique ID for the re-indexing operation. - /// Indicates whether completed tags should also be returned. - /// - /// The token to monitor for cancellation requests. The default value is . - /// - /// - /// A task representing the operation. - /// The value of its property contains the subset of query tags that were - /// successfully assigned to the . - /// - /// is empty. - /// is . - /// The was canceled. - Task> AssignReindexingOperationAsync( - IReadOnlyCollection queryTagKeys, - Guid operationId, - bool returnIfCompleted = false, - CancellationToken cancellationToken = default); - - /// - /// Asynchronously marks the re-indexing operation as complete for the given extended query tags. - /// - /// The keys for the extended query tags. - /// - /// The token to monitor for cancellation requests. The default value is . - /// - /// - /// A task representing the operation. - /// The value of its property contains the set of query tags whose - /// status was successfully updated to "complete." - /// - /// is empty. - /// is . - /// The was canceled. - Task> CompleteReindexingAsync(IReadOnlyCollection queryTagKeys, CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/IGetExtendedQueryTagsService.cs b/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/IGetExtendedQueryTagsService.cs deleted file mode 100644 index 41131a79f3..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/IGetExtendedQueryTagsService.cs +++ /dev/null @@ -1,36 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Health.Dicom.Core.Messages.ExtendedQueryTag; - -namespace Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -public interface IGetExtendedQueryTagsService -{ - /// - /// Gets requested Extended Query Tag. - /// - /// Path to the extended query tag that is requested. - /// The cancellation token. - /// The response. - public Task GetExtendedQueryTagAsync(string tagPath, CancellationToken cancellationToken = default); - - /// - /// Gets all stored Extended Query Tags. - /// - /// The maximum number of results to retrieve. - /// The offset from which to retrieve paginated results. - /// The cancellation token. - /// The response. - /// - /// is less than 1 - /// -or- - /// is less than 0. - /// - public Task GetExtendedQueryTagsAsync(int limit, long offset = 0, CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/IQueryTagService.cs b/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/IQueryTagService.cs deleted file mode 100644 index 23484539ff..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/IQueryTagService.cs +++ /dev/null @@ -1,23 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -/// -/// Service provides queryable dicom tags. -/// -public interface IQueryTagService -{ - /// - /// Get queryable dicom tags. - /// - /// The cancellation token. - /// Queryable dicom tags. - Task> GetQueryTagsAsync(CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/IUpdateExtendedQueryTagService.cs b/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/IUpdateExtendedQueryTagService.cs deleted file mode 100644 index 5c7b11ce95..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/IUpdateExtendedQueryTagService.cs +++ /dev/null @@ -1,21 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -public interface IUpdateExtendedQueryTagService -{ - /// - /// Update extended query tag. - /// - /// The tag path. - /// The new value. - /// The cancellation token. - /// The return tag entry. - public Task UpdateExtendedQueryTagAsync(string tagPath, UpdateExtendedQueryTagEntry newValue, CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/QueryStatus.cs b/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/QueryStatus.cs deleted file mode 100644 index e16f37961e..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/QueryStatus.cs +++ /dev/null @@ -1,25 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -/// -/// Query status of query tag. -/// -[SuppressMessage("Design", "CA1028:Enum Storage should be Int32", Justification = "Vaule is stored in SQL as TINYINT")] -public enum QueryStatus : byte -{ - /// - /// The tag is not allowed to be queried. - /// - Disabled = 0, - - /// - /// The tag is allowed to be queried. - /// - Enabled = 1, -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/QueryTag.cs b/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/QueryTag.cs deleted file mode 100644 index 8bd515dd18..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/QueryTag.cs +++ /dev/null @@ -1,106 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Query; -using Microsoft.Health.Dicom.Core.Features.Workitem; - -namespace Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -/// -/// Queryable Dicom Tag. -/// -public class QueryTag -{ - /// - /// Initializes a new instance of the class. - /// - /// Used for constuctoring from core dicom tag.PatientName e.g. - /// The core dicom Tag. - public QueryTag(DicomTag tag) - { - EnsureArg.IsNotNull(tag, nameof(tag)); - - Tag = tag; - VR = tag.GetDefaultVR(); - Level = QueryLimit.GetQueryTagLevel(tag); - ExtendedQueryTagStoreEntry = null; - } - - /// - /// Initializes a new instance of the class. - /// - /// Used for constuctoring from extended query tags. - /// The extended query tag store entry. - public QueryTag(ExtendedQueryTagStoreEntry entry) - { - EnsureArg.IsNotNull(entry, nameof(entry)); - string fullPath = string.IsNullOrEmpty(entry.PrivateCreator) ? entry.Path : $"{entry.Path}:{entry.PrivateCreator}"; - Tag = DicomTag.Parse(fullPath); - VR = DicomVR.Parse(entry.VR); - Level = entry.Level; - ExtendedQueryTagStoreEntry = entry; - } - - /// - /// Initializes a new instance of the class. - /// - /// Used for constructing from (to model sequences). - /// The WorkitemQueryTagStore entry. - public QueryTag(WorkitemQueryTagStoreEntry entry) - { - EnsureArg.IsNotNull(entry, nameof(entry)); - - Tag = DicomTag.Parse(entry.Path); - VR = DicomVR.Parse(entry.VR); - WorkitemQueryTagStoreEntry = entry; - } - - /// - /// Gets Dicom Tag. - /// - public DicomTag Tag { get; } - - /// - /// Gets Dicom VR. - /// - public DicomVR VR { get; } - - /// - /// Gets Dicom Tag Level. - /// - public QueryTagLevel Level { get; } - - /// - /// Gets whether this is extended query tag or not. - /// - public bool IsExtendedQueryTag => ExtendedQueryTagStoreEntry != null; - - /// - /// Gets the underlying extendedQueryTagStoreEntry for extended query tag. - /// - public ExtendedQueryTagStoreEntry ExtendedQueryTagStoreEntry { get; } - - /// - /// Gets the underlying workitemQueryTagStoreEntry for workitem query tag. - /// - public WorkitemQueryTagStoreEntry WorkitemQueryTagStoreEntry { get; } - - /// - /// Gets whether this is workitem query tag or not. - /// - public bool IsWorkitemQueryTag => WorkitemQueryTagStoreEntry != null; - - /// - /// Gets name of this query tag. - /// - /// - public string GetName() - { - return Tag.DictionaryEntry == DicomDictionary.UnknownTag ? Tag.GetPath() : Tag.DictionaryEntry.Keyword; - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/QueryTagEntry.cs b/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/QueryTagEntry.cs deleted file mode 100644 index e2152936bc..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/QueryTagEntry.cs +++ /dev/null @@ -1,23 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -/// -/// Representation of a query tag entry. -/// -public abstract class QueryTagEntry -{ - /// - /// Path of this tag. Normally it's composed of groupid and elementid. - /// E.g: 00100020 is path of patient id. - /// - public string Path { get; set; } - - /// - /// VR of this tag. - /// - public string VR { get; set; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/QueryTagLevel.cs b/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/QueryTagLevel.cs deleted file mode 100644 index 5937f68eda..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/QueryTagLevel.cs +++ /dev/null @@ -1,27 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -/// -/// Level of a query tag. -/// -public enum QueryTagLevel -{ - /// - /// The query tag is on instance level. - /// - Instance = 0, - - /// - /// The query tag is on series level. - /// - Series = 1, - - /// - /// The query tag is on study level. - /// - Study = 2, -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/QueryTagResourceType.cs b/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/QueryTagResourceType.cs deleted file mode 100644 index de611b27cd..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/QueryTagResourceType.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -/// -/// Resource type of a query tag. -/// -public enum QueryTagResourceType -{ - /// - /// The image instance resource type - /// - Image = 0, - - /// - /// The workitem instance resource type - /// - Workitem = 1 -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/QueryTagService.cs b/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/QueryTagService.cs deleted file mode 100644 index 9f08249bc6..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/QueryTagService.cs +++ /dev/null @@ -1,47 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.Health.Dicom.Core.Features.Query; -using Microsoft.Health.Dicom.Features.Common; - -namespace Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -public sealed class QueryTagService : IQueryTagService, IDisposable -{ - private readonly IExtendedQueryTagStore _extendedQueryTagStore; - private readonly AsyncCache> _queryTagCache; - - public QueryTagService(IExtendedQueryTagStore extendedQueryTagStore) - { - _extendedQueryTagStore = EnsureArg.IsNotNull(extendedQueryTagStore, nameof(extendedQueryTagStore)); - _queryTagCache = new AsyncCache>(ResolveQueryTagsAsync); - } - - public static IReadOnlyList CoreQueryTags { get; } = QueryLimit.CoreFilterTags.Select(tag => new QueryTag(tag)).ToList(); - - public void Dispose() - { - _queryTagCache.Dispose(); - GC.SuppressFinalize(this); - } - - public Task> GetQueryTagsAsync(CancellationToken cancellationToken = default) - => _queryTagCache.GetAsync(cancellationToken: cancellationToken); - - private async Task> ResolveQueryTagsAsync(CancellationToken cancellationToken) - { - var tags = new List(CoreQueryTags); - IReadOnlyList extendedQueryTags = await _extendedQueryTagStore.GetExtendedQueryTagsAsync(int.MaxValue, 0, cancellationToken); - tags.AddRange(extendedQueryTags.Select(entry => new QueryTag(entry))); - - return tags; - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/UpdateExtendedQueryTagEntry.cs b/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/UpdateExtendedQueryTagEntry.cs deleted file mode 100644 index 512c289bb7..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/UpdateExtendedQueryTagEntry.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; - -namespace Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -/// -/// Encapsulate parameters for updating extended query tag. -/// -public class UpdateExtendedQueryTagEntry -{ - public UpdateExtendedQueryTagEntry(QueryStatus queryStatus) - { - QueryStatus = EnsureArg.EnumIsDefined(queryStatus, nameof(queryStatus)); - } - - /// - /// Gets or sets query status. - /// - public QueryStatus QueryStatus { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/UpdateExtendedQueryTagQueryStatusHandler.cs b/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/UpdateExtendedQueryTagQueryStatusHandler.cs deleted file mode 100644 index 1f0205e06c..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/UpdateExtendedQueryTagQueryStatusHandler.cs +++ /dev/null @@ -1,40 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using MediatR; -using Microsoft.Health.Core.Features.Security.Authorization; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Security; -using Microsoft.Health.Dicom.Core.Messages.ExtendedQueryTag; - -namespace Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -public class UpdateExtendedQueryTagQueryStatusHandler : BaseHandler, IRequestHandler -{ - private readonly IUpdateExtendedQueryTagService _updateTagService; - - public UpdateExtendedQueryTagQueryStatusHandler(IAuthorizationService authorizationService, IUpdateExtendedQueryTagService updateTagService) - : base(authorizationService) - { - _updateTagService = EnsureArg.IsNotNull(updateTagService, nameof(updateTagService)); - } - - public async Task Handle(UpdateExtendedQueryTagRequest request, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(request, nameof(request)); - - if (await AuthorizationService.CheckAccess(DataActions.ManageExtendedQueryTags, cancellationToken) != DataActions.ManageExtendedQueryTags) - { - throw new UnauthorizedDicomActionException(DataActions.ManageExtendedQueryTags); - } - - var tagEntry = await _updateTagService.UpdateExtendedQueryTagAsync(request.TagPath, request.NewValue, cancellationToken); - return new UpdateExtendedQueryTagResponse(tagEntry); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/UpdateExtendedQueryTagService.cs b/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/UpdateExtendedQueryTagService.cs deleted file mode 100644 index 88f6646c46..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/ExtendedQueryTag/UpdateExtendedQueryTagService.cs +++ /dev/null @@ -1,45 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Globalization; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Routing; - -namespace Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -public class UpdateExtendedQueryTagService : IUpdateExtendedQueryTagService -{ - private readonly IExtendedQueryTagStore _extendedQueryTagStore; - private readonly IDicomTagParser _dicomTagParser; - private readonly IUrlResolver _urlResolver; - - public UpdateExtendedQueryTagService(IExtendedQueryTagStore extendedQueryTagStore, IDicomTagParser dicomTagParser, IUrlResolver urlResolver) - { - _extendedQueryTagStore = EnsureArg.IsNotNull(extendedQueryTagStore, nameof(extendedQueryTagStore)); - _dicomTagParser = EnsureArg.IsNotNull(dicomTagParser, nameof(dicomTagParser)); - _urlResolver = EnsureArg.IsNotNull(urlResolver, nameof(urlResolver)); - } - - public async Task UpdateExtendedQueryTagAsync(string tagPath, UpdateExtendedQueryTagEntry newValue, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(tagPath, nameof(tagPath)); - EnsureArg.IsNotNull(newValue?.QueryStatus, nameof(newValue)); - EnsureArg.EnumIsDefined(newValue.QueryStatus, nameof(UpdateExtendedQueryTagEntry.QueryStatus)); - if (!_dicomTagParser.TryParse(tagPath, out DicomTag[] tags)) - { - throw new InvalidExtendedQueryTagPathException( - string.Format(CultureInfo.InvariantCulture, DicomCoreResource.InvalidExtendedQueryTag, tagPath ?? string.Empty)); - } - string normalizedPath = tags[0].GetPath(); - var entry = await _extendedQueryTagStore.UpdateQueryStatusAsync(normalizedPath, newValue.QueryStatus, cancellationToken); - return entry.ToGetExtendedQueryTagEntry(_urlResolver); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/FellowOakDicom/CustomDicomImplementation.cs b/src/Microsoft.Health.Dicom.Core/Features/FellowOakDicom/CustomDicomImplementation.cs deleted file mode 100644 index 7a3c9c1b89..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/FellowOakDicom/CustomDicomImplementation.cs +++ /dev/null @@ -1,34 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Reflection; -using FellowOakDicom; - -namespace Microsoft.Health.Dicom.Core.Features.FellowOakDicom; - -/// -/// Represents a custom DICOM implementation. -/// -public static class CustomDicomImplementation -{ - /// - /// Azure Health Data Services specific OID registered under Microsoft OID arc. - /// Used to identify the DICOM implementation Class UID. - /// - private const string ImplementationClassUid = "1.3.6.1.4.1.311.129"; - - /// - /// This method sets the DICOM implementation class UID and version. - /// ImplementationClassUID and ImplementationVersion are used to identify the software that generated or last touched the data. - /// - public static void SetDicomImplementationClassUIDAndVersion() - { - Assembly assembly = typeof(CustomDicomImplementation).GetTypeInfo().Assembly; - AssemblyFileVersionAttribute fileVersionAttribute = assembly.GetCustomAttribute(); - - DicomImplementation.ClassUID = new DicomUID(ImplementationClassUid, "Implementation Class UID", DicomUidType.Unknown); - DicomImplementation.Version = fileVersionAttribute?.Version ?? "Unknown"; - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Indexing/IInstanceReindexer.cs b/src/Microsoft.Health.Dicom.Core/Features/Indexing/IInstanceReindexer.cs deleted file mode 100644 index 6aab751a5f..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Indexing/IInstanceReindexer.cs +++ /dev/null @@ -1,28 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Features.Model; - -namespace Microsoft.Health.Dicom.Core.Features.Indexing; - -/// -/// Represents an Reindexer which reindexes DICOM instance. -/// -public interface IInstanceReindexer -{ - /// - /// Asynchronously reindexes the DICOM instance with the - /// for the set of . - /// - /// Extended query tag store entries. - /// The versioned instance id. - /// The cancellation token. - /// A representing the asynchronous operation. - Task ReindexInstanceAsync(IReadOnlyCollection entries, VersionedInstanceIdentifier versionedInstanceId, CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Indexing/IReindexDatasetValidator.cs b/src/Microsoft.Health.Dicom.Core/Features/Indexing/IReindexDatasetValidator.cs deleted file mode 100644 index bc35aa5890..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Indexing/IReindexDatasetValidator.cs +++ /dev/null @@ -1,28 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -namespace Microsoft.Health.Dicom.Core.Features.Indexing; - -/// -/// Validator that validates DicomDataset for reindexing. -/// -public interface IReindexDatasetValidator -{ - /// - /// Validate . - /// - /// The dicom Dataset. - /// The Dicom instance watermark. - /// The query tags. - /// The cancellation token - /// Valid query tags. - Task> ValidateAsync(DicomDataset dataset, long watermark, IReadOnlyCollection queryTags, CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Indexing/InstanceReindexer.cs b/src/Microsoft.Health.Dicom.Core/Features/Indexing/InstanceReindexer.cs deleted file mode 100644 index 9d5696d9da..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Indexing/InstanceReindexer.cs +++ /dev/null @@ -1,56 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Features.Store; - -namespace Microsoft.Health.Dicom.Core.Features.Indexing; - -/// -/// Represents an Reindexer which reindexes DICOM instance. -/// -public class InstanceReindexer : IInstanceReindexer -{ - private readonly IMetadataStore _metadataStore; - private readonly IIndexDataStore _indexDataStore; - private readonly IReindexDatasetValidator _dicomDatasetReindexValidator; - - public InstanceReindexer( - IMetadataStore metadataStore, - IIndexDataStore indexDataStore, - IReindexDatasetValidator dicomDatasetReindexValidator) - { - _metadataStore = EnsureArg.IsNotNull(metadataStore, nameof(metadataStore)); - _indexDataStore = EnsureArg.IsNotNull(indexDataStore, nameof(indexDataStore)); - _dicomDatasetReindexValidator = EnsureArg.IsNotNull(dicomDatasetReindexValidator, nameof(dicomDatasetReindexValidator)); - } - - public async Task ReindexInstanceAsync( - IReadOnlyCollection entries, - VersionedInstanceIdentifier versionedInstanceId, - CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(entries, nameof(entries)); - EnsureArg.IsNotNull(versionedInstanceId, nameof(versionedInstanceId)); - - DicomDataset dataset = await _metadataStore.GetInstanceMetadataAsync(versionedInstanceId.Version, cancellationToken); - - // Only reindex on valid query tags - IReadOnlyCollection validQueryTags = await _dicomDatasetReindexValidator.ValidateAsync( - dataset, - versionedInstanceId.Version, - entries.Select(x => new QueryTag(x)).ToList(), - cancellationToken); - await _indexDataStore.ReindexInstanceAsync(dataset, versionedInstanceId.Version, validQueryTags, cancellationToken); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Indexing/ReindexDatasetValidator.cs b/src/Microsoft.Health.Dicom.Core/Features/Indexing/ReindexDatasetValidator.cs deleted file mode 100644 index 3e25a79d69..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Indexing/ReindexDatasetValidator.cs +++ /dev/null @@ -1,54 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Features.Validation; - -namespace Microsoft.Health.Dicom.Core.Features.Indexing; - -public class ReindexDatasetValidator : IReindexDatasetValidator -{ - private readonly IElementMinimumValidator _minimumValidator; - private readonly IExtendedQueryTagErrorsService _extendedQueryTagErrorsService; - - public ReindexDatasetValidator(IElementMinimumValidator minimumValidator, IExtendedQueryTagErrorsService extendedQueryTagErrorsService) - { - _minimumValidator = EnsureArg.IsNotNull(minimumValidator, nameof(minimumValidator)); - _extendedQueryTagErrorsService = EnsureArg.IsNotNull(extendedQueryTagErrorsService, nameof(extendedQueryTagErrorsService)); - } - - public async Task> ValidateAsync(DicomDataset dataset, long watermark, IReadOnlyCollection queryTags, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(dataset, nameof(dataset)); - EnsureArg.IsNotNull(queryTags, nameof(queryTags)); - - List validTags = new List(); - foreach (var queryTag in queryTags) - { - try - { - // Ignore validation warnings until we figure out proper way to handle with. - dataset.ValidateQueryTag(queryTag, _minimumValidator); - validTags.Add(queryTag); - } - catch (ElementValidationException ex) - { - if (queryTag.IsExtendedQueryTag) - { - // We don't support reindex on core tag, so the query tag is always extended query tag. - await _extendedQueryTagErrorsService.AddExtendedQueryTagErrorAsync(queryTag.ExtendedQueryTagStoreEntry.Key, ex.ErrorCode, watermark, cancellationToken); - } - } - } - return validTags; - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Model/FrameRange.cs b/src/Microsoft.Health.Dicom.Core/Features/Model/FrameRange.cs deleted file mode 100644 index 2db5a29868..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Model/FrameRange.cs +++ /dev/null @@ -1,18 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Features.Model; - -public class FrameRange -{ - public FrameRange(long offset, long length) - { - Offset = offset; - Length = length; - } - - public long Offset { get; } - public long Length { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Model/InstanceFileIdentifier.cs b/src/Microsoft.Health.Dicom.Core/Features/Model/InstanceFileIdentifier.cs deleted file mode 100644 index 78e7152f0e..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Model/InstanceFileIdentifier.cs +++ /dev/null @@ -1,31 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Features.Model; - -/// -/// Represents a file identifier for an instance. -/// -public class InstanceFileState -{ - /// - /// This corresponds to the current version of the instance. This is similar to - /// - public long Version { get; init; } - - /// - /// This corresponds to the original version of the instance if the DICOM instance was updated. - /// Used to fetch the dicom file using this version if the original version is requested. - /// This is similar to - /// - public long? OriginalVersion { get; init; } - - /// - /// This corresponds to the future current version of the instance while the DICOM instance is being updated. - /// This is only used for the duration of the update operation. - /// - /// This is similar to - public long? NewVersion { get; init; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Model/InstanceIdentifier.cs b/src/Microsoft.Health.Dicom.Core/Features/Model/InstanceIdentifier.cs deleted file mode 100644 index 9cd4258b51..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Model/InstanceIdentifier.cs +++ /dev/null @@ -1,52 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using EnsureThat; -using Microsoft.Health.Dicom.Core.Features.Partitioning; - -namespace Microsoft.Health.Dicom.Core.Features.Model; - -public class InstanceIdentifier -{ - private const StringComparison EqualsStringComparison = StringComparison.Ordinal; - - public InstanceIdentifier( - string studyInstanceUid, - string seriesInstanceUid, - string sopInstanceUid, - Partition partition) - { - Partition = EnsureArg.IsNotNull(partition, nameof(partition)); - StudyInstanceUid = EnsureArg.IsNotNullOrWhiteSpace(studyInstanceUid, nameof(studyInstanceUid)); - SeriesInstanceUid = EnsureArg.IsNotNullOrWhiteSpace(seriesInstanceUid, nameof(seriesInstanceUid)); - SopInstanceUid = EnsureArg.IsNotNullOrWhiteSpace(sopInstanceUid, nameof(sopInstanceUid)); - } - - public string StudyInstanceUid { get; } - - public string SeriesInstanceUid { get; } - - public string SopInstanceUid { get; } - - public Partition Partition { get; } - - public override bool Equals(object obj) - { - if (obj is InstanceIdentifier identifier) - { - return StudyInstanceUid.Equals(identifier.StudyInstanceUid, EqualsStringComparison) && - SeriesInstanceUid.Equals(identifier.SeriesInstanceUid, EqualsStringComparison) && - SopInstanceUid.Equals(identifier.SopInstanceUid, EqualsStringComparison); - } - return false; - } - - public override int GetHashCode() - => (Partition.Key + StudyInstanceUid + SeriesInstanceUid + SopInstanceUid).GetHashCode(EqualsStringComparison); - - public override string ToString() - => $"PartitionKey: {Partition.Key}, StudyInstanceUID: {StudyInstanceUid}, SeriesInstanceUID: {SeriesInstanceUid}, SOPInstanceUID: {SopInstanceUid}"; -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Model/InstanceMetadata.cs b/src/Microsoft.Health.Dicom.Core/Features/Model/InstanceMetadata.cs deleted file mode 100644 index 592a80326b..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Model/InstanceMetadata.cs +++ /dev/null @@ -1,41 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; - -namespace Microsoft.Health.Dicom.Core.Features.Model; - -public class InstanceMetadata -{ - public InstanceMetadata(VersionedInstanceIdentifier versionedInstanceIdentifier, InstanceProperties instanceProperties) - { - VersionedInstanceIdentifier = EnsureArg.IsNotNull(versionedInstanceIdentifier, nameof(versionedInstanceIdentifier)); - InstanceProperties = EnsureArg.IsNotNull(instanceProperties, nameof(instanceProperties)); - } - - public VersionedInstanceIdentifier VersionedInstanceIdentifier { get; } - - public InstanceProperties InstanceProperties { get; } - - public long GetVersion(bool isOriginalVersionRequested) - { - if (isOriginalVersionRequested && InstanceProperties.OriginalVersion.HasValue) - { - return InstanceProperties.OriginalVersion.Value; - } - - return VersionedInstanceIdentifier.Version; - } - - public InstanceFileState ToInstanceFileState() - { - return new InstanceFileState - { - Version = VersionedInstanceIdentifier.Version, - OriginalVersion = InstanceProperties.OriginalVersion, - NewVersion = InstanceProperties.NewVersion - }; - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Model/InstanceProperties.cs b/src/Microsoft.Health.Dicom.Core/Features/Model/InstanceProperties.cs deleted file mode 100644 index 65d902e99c..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Model/InstanceProperties.cs +++ /dev/null @@ -1,40 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Microsoft.Health.Dicom.Core.Features.Common; - -namespace Microsoft.Health.Dicom.Core.Features.Model; - -public class InstanceProperties -{ - /// - /// Transfer syntax uid of instance - /// - public string TransferSyntaxUid { get; init; } - - /// - /// True if the instance has frame metadata - /// - public bool HasFrameMetadata { get; init; } - - /// - /// This corresponds to the original version of the instance if the DICOM instance was updated. - /// Used to fetch the dicom file using this version if the original version is requested. - /// - /// This is referenced in - public long? OriginalVersion { get; init; } - - /// - /// This corresponds to the future current version of the instance while the DICOM instance is being updated. - /// This is only used for the duration of the update operation. - /// - /// This is referenced in - public long? NewVersion { get; init; } - - /// - /// File properties of instance - /// - public FileProperties FileProperties { get; init; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Model/VersionedInstanceIdentifier.cs b/src/Microsoft.Health.Dicom.Core/Features/Model/VersionedInstanceIdentifier.cs deleted file mode 100644 index 44988b43b3..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Model/VersionedInstanceIdentifier.cs +++ /dev/null @@ -1,40 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Microsoft.Health.Dicom.Core.Features.Partitioning; - -namespace Microsoft.Health.Dicom.Core.Features.Model; - -public class VersionedInstanceIdentifier : InstanceIdentifier -{ - public VersionedInstanceIdentifier( - string studyInstanceUid, - string seriesInstanceUid, - string sopInstanceUid, - long version, - Partition partition = null) - : base(studyInstanceUid, seriesInstanceUid, sopInstanceUid, partition ?? Partition.Default) - { - Version = version; - } - - public long Version { get; } - - public override bool Equals(object obj) - { - if (obj is VersionedInstanceIdentifier identifier) - { - return base.Equals(obj) && identifier.Version == Version; - } - - return false; - } - - public override int GetHashCode() - => base.GetHashCode() ^ Version.GetHashCode(); - - public override string ToString() - => base.ToString() + $", Version: {Version}"; -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Model/WatermarkRange.cs b/src/Microsoft.Health.Dicom.Core/Features/Model/WatermarkRange.cs deleted file mode 100644 index ccc64b46f1..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Model/WatermarkRange.cs +++ /dev/null @@ -1,57 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Diagnostics; -using EnsureThat; - -namespace Microsoft.Health.Dicom.Core.Features.Model; - -/// -/// Represents a range of DICOM instance watermarks. -/// -[DebuggerDisplay("{ToString(),nq}")] -public readonly struct WatermarkRange : IEquatable -{ - public WatermarkRange(long start, long end) - { - Start = EnsureArg.IsGte(start, 1, nameof(start)); - End = EnsureArg.IsGte(end, start, nameof(end)); - } - - /// - /// Gets inclusive starting instance watermark. - /// - public long Start { get; } - - /// - /// Gets inclusive ending instance watermark. - /// - public long End { get; } - - public override bool Equals(object obj) - => obj is WatermarkRange other && Equals(other); - - public override int GetHashCode() - => HashCode.Combine(Start, End); - - public static bool operator ==(WatermarkRange left, WatermarkRange right) - => left.Equals(right); - - public static bool operator !=(WatermarkRange left, WatermarkRange right) - => !(left == right); - - public bool Equals(WatermarkRange other) - => Start == other.Start && End == other.End; - - public void Deconstruct(out long start, out long end) - { - start = Start; - end = End; - } - - public override string ToString() - => "[" + Start + ", " + End + "]"; -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Operations/DicomOperationsResourceStore.cs b/src/Microsoft.Health.Dicom.Core/Features/Operations/DicomOperationsResourceStore.cs deleted file mode 100644 index d8c8a2ecc5..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Operations/DicomOperationsResourceStore.cs +++ /dev/null @@ -1,33 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Threading; -using EnsureThat; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -namespace Microsoft.Health.Dicom.Core.Features.Operations; - -internal sealed class DicomOperationsResourceStore : IDicomOperationsResourceStore -{ - private readonly IExtendedQueryTagStore _queryTagStore; - - public DicomOperationsResourceStore(IExtendedQueryTagStore queryTagStore) - => _queryTagStore = EnsureArg.IsNotNull(queryTagStore, nameof(queryTagStore)); - - public async IAsyncEnumerable ResolveQueryTagKeysAsync(IReadOnlyCollection keys, [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - EnsureArg.IsNotNull(keys, nameof(keys)); - EnsureArg.HasItems(keys, nameof(keys)); - - IReadOnlyList entries = await _queryTagStore.GetExtendedQueryTagsAsync(keys, cancellationToken); - foreach (string path in entries.Select(x => x.Path)) - { - yield return path; - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Operations/IDicomOperationsClient.cs b/src/Microsoft.Health.Dicom.Core/Features/Operations/IDicomOperationsClient.cs deleted file mode 100644 index abe1f0af73..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Operations/IDicomOperationsClient.cs +++ /dev/null @@ -1,144 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Core.Models.Export; -using Microsoft.Health.Dicom.Core.Models.Operations; -using Microsoft.Health.Dicom.Core.Models.Update; -using Microsoft.Health.Operations; - -namespace Microsoft.Health.Dicom.Core.Features.Operations; - -/// -/// Represents a client for interacting with long-running DICOM operations. -/// -public interface IDicomOperationsClient -{ - /// - /// Asynchronously retrieves the state of a long-running operation for the given . - /// - /// The unique ID for a particular DICOM operation. - /// - /// The token to monitor for cancellation requests. The default value is . - /// - /// - /// A task representing the operation. The value of its - /// property contains the state of the operation - /// with the specified , if found; otherwise . - /// - /// The was canceled. - Task> GetStateAsync(Guid operationId, CancellationToken cancellationToken = default); - - /// - /// Asynchronously searches for long-running operations based on the given . - /// - /// A set of operation search criteria. - /// - /// The token to monitor for cancellation requests. The default value is . - /// - /// An asynchronous enumeration of results based on the . - /// The was canceled. - IAsyncEnumerable FindOperationsAsync(OperationQueryCondition query, CancellationToken cancellationToken = default); - - /// - /// Asynchronously retrieves the state of a long-running operation for the given with checkpoint information. - /// - /// The unique ID for a particular DICOM operation. - /// - /// The token to monitor for cancellation requests. The default value is . - /// - /// - /// A task representing the operation. The value of its - /// property contains the state of the operation - /// with the specified , if found; otherwise . - /// - /// The was canceled. - Task> GetLastCheckpointAsync(Guid operationId, CancellationToken cancellationToken = default); - - /// - /// Asynchronously begins the re-indexing of existing DICOM instances on the tags with the specified . - /// - /// The desired ID for the long-running re-index operation. - /// A collection of 1 or more existing query tag keys. - /// - /// The token to monitor for cancellation requests. The default value is . - /// - /// - /// A task representing the - /// operation. The value of its property contains a reference - /// to the newly started operation. - /// - /// is empty. - /// is . - /// The was canceled. - Task StartReindexingInstancesAsync(Guid operationId, IReadOnlyCollection tagKeys, CancellationToken cancellationToken = default); - - /// - /// Asynchronously begins the export of files as detailed in the given . - /// - /// The desired ID for the long-running export operation. - /// The specification that details the source and destination for the export. - /// The partition containing the data to export. - /// The for the export error log. - /// - /// The token to monitor for cancellation requests. The default value is . - /// - /// - /// A task representing the - /// operation. The value of its property contains a reference - /// to the newly started operation. - /// - /// - /// , , or is . - /// - /// The was canceled. - Task StartExportAsync(Guid operationId, ExportSpecification specification, Uri errorHref, Partition partition, CancellationToken cancellationToken = default); - - /// - /// Asynchronously begins the update operation in the given . - /// - /// The desired ID for the long-running update operation. - /// The specification that details the update changed dataset for updating studies - /// The partition containing the data to update. - /// The token to monitor for cancellation requests. The default value is . - /// - /// A task representing the - /// operation. The value of its property contains a reference - /// to the newly started operation. - /// - /// - /// is . - /// - /// The was canceled. - Task StartUpdateOperationAsync(Guid operationId, UpdateSpecification updateSpecification, Partition partition, CancellationToken cancellationToken = default); - - /// - /// Asynchronously begins the clean up of instance data. - /// - /// The desired ID for the cleanup operation. - /// Start timestamp to filter instances. - /// End timestamp to filter instances. - /// The token to monitor for cancellation requests. The default value is . - /// - /// A task representing the operation. - /// - /// The was canceled. - Task StartInstanceDataCleanupOperationAsync(Guid operationId, DateTimeOffset startFilterTimeStamp, DateTimeOffset endFilterTimeStamp, CancellationToken cancellationToken = default); - - /// - /// Asynchronously begins the backfill of content length for DICOM instances. - /// - /// The desired ID for the operation. - /// The token to monitor for cancellation requests. The default value is . - /// - /// A task representing the operation. - /// - /// The was canceled. - Task StartContentLengthBackFillOperationAsync(Guid operationId, CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Operations/IDicomOperationsResourceStore.cs b/src/Microsoft.Health.Dicom.Core/Features/Operations/IDicomOperationsResourceStore.cs deleted file mode 100644 index c28a73dfa5..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Operations/IDicomOperationsResourceStore.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Threading; - -namespace Microsoft.Health.Dicom.Core.Features.Operations; - -internal interface IDicomOperationsResourceStore -{ - IAsyncEnumerable ResolveQueryTagKeysAsync(IReadOnlyCollection keys, CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Operations/OperationStateHandler.cs b/src/Microsoft.Health.Dicom.Core/Features/Operations/OperationStateHandler.cs deleted file mode 100644 index 9b6a8594c6..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Operations/OperationStateHandler.cs +++ /dev/null @@ -1,66 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using MediatR; -using Microsoft.Health.Core.Features.Security.Authorization; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Security; -using Microsoft.Health.Dicom.Core.Messages.Operations; -using Microsoft.Health.Dicom.Core.Models.Operations; -using Microsoft.Health.Operations; - -namespace Microsoft.Health.Dicom.Core.Features.Operations; - -/// -/// Represents a handler that encapsulates -/// to process instances of . -/// -public class OperationStateHandler : BaseHandler, IRequestHandler -{ - private readonly IDicomOperationsClient _client; - - /// - /// Initializes a new instance of the class. - /// - /// A service for determining if a user is authorized. - /// A client for interacting with DICOM operations. - /// is . - public OperationStateHandler(IAuthorizationService authorizationService, IDicomOperationsClient client) - : base(authorizationService) - => _client = EnsureArg.IsNotNull(client, nameof(client)); - - /// - /// Invokes by forwarding the - /// and returns its response. - /// - /// A request for the state of a particular DICOM operation. - /// - /// The token to monitor for cancellation requests. The default value is . - /// - /// - /// A task representing the operation. - /// The value of its property contains the state of the operation - /// based on the , if found; otherwise . - /// - /// is . - /// The was canceled. - public async Task Handle(OperationStateRequest request, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(request, nameof(request)); - - if (await AuthorizationService.CheckAccess(DataActions.Read, cancellationToken) != DataActions.Read) - { - throw new UnauthorizedDicomActionException(DataActions.Read); - } - - IOperationState state = await _client.GetStateAsync(request.OperationId, cancellationToken); - return state != null ? new OperationStateResponse(state) : null; - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Partitioning/GetOrAddPartitionHandler.cs b/src/Microsoft.Health.Dicom.Core/Features/Partitioning/GetOrAddPartitionHandler.cs deleted file mode 100644 index 7ca9b8d744..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Partitioning/GetOrAddPartitionHandler.cs +++ /dev/null @@ -1,39 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using MediatR; -using Microsoft.Health.Core.Features.Security.Authorization; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Security; -using Microsoft.Health.Dicom.Core.Messages.Partitioning; - -namespace Microsoft.Health.Dicom.Core.Features.Partitioning; - -public class GetOrAddPartitionHandler : BaseHandler, IRequestHandler -{ - private readonly IPartitionService _partitionService; - - public GetOrAddPartitionHandler(IAuthorizationService authorizationService, IPartitionService partitionService) - : base(authorizationService) - { - _partitionService = EnsureArg.IsNotNull(partitionService, nameof(partitionService)); - } - - public async Task Handle(GetOrAddPartitionRequest request, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(request, nameof(request)); - - if (await AuthorizationService.CheckAccess(DataActions.Write, cancellationToken) != DataActions.Write) - { - throw new UnauthorizedDicomActionException(DataActions.Write); - } - - return await _partitionService.GetOrAddPartitionAsync(request.PartitionName, cancellationToken); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Partitioning/GetPartitionHandler.cs b/src/Microsoft.Health.Dicom.Core/Features/Partitioning/GetPartitionHandler.cs deleted file mode 100644 index 165e308470..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Partitioning/GetPartitionHandler.cs +++ /dev/null @@ -1,39 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using MediatR; -using Microsoft.Health.Core.Features.Security.Authorization; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Security; -using Microsoft.Health.Dicom.Core.Messages.Partitioning; - -namespace Microsoft.Health.Dicom.Core.Features.Partitioning; - -public class GetPartitionHandler : BaseHandler, IRequestHandler -{ - private readonly IPartitionService _partitionService; - - public GetPartitionHandler(IAuthorizationService authorizationService, IPartitionService partitionService) - : base(authorizationService) - { - _partitionService = EnsureArg.IsNotNull(partitionService, nameof(partitionService)); - } - - public async Task Handle(GetPartitionRequest request, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(request, nameof(request)); - - if (await AuthorizationService.CheckAccess(DataActions.Read, cancellationToken) != DataActions.Read) - { - throw new UnauthorizedDicomActionException(DataActions.Read); - } - - return await _partitionService.GetPartitionAsync(request.PartitionName, cancellationToken); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Partitioning/GetPartitionsHandler.cs b/src/Microsoft.Health.Dicom.Core/Features/Partitioning/GetPartitionsHandler.cs deleted file mode 100644 index 8367c1c0db..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Partitioning/GetPartitionsHandler.cs +++ /dev/null @@ -1,39 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using MediatR; -using Microsoft.Health.Core.Features.Security.Authorization; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Security; -using Microsoft.Health.Dicom.Core.Messages.Partitioning; - -namespace Microsoft.Health.Dicom.Core.Features.Partitioning; - -public class GetPartitionsHandler : BaseHandler, IRequestHandler -{ - private readonly IPartitionService _partitionService; - - public GetPartitionsHandler(IAuthorizationService authorizationService, IPartitionService partitionService) - : base(authorizationService) - { - _partitionService = EnsureArg.IsNotNull(partitionService, nameof(partitionService)); - } - - public async Task Handle(GetPartitionsRequest request, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(request, nameof(request)); - - if (await AuthorizationService.CheckAccess(DataActions.Read, cancellationToken) != DataActions.Read) - { - throw new UnauthorizedDicomActionException(DataActions.Read); - } - - return await _partitionService.GetPartitionsAsync(cancellationToken); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Partitioning/IPartitionService.cs b/src/Microsoft.Health.Dicom.Core/Features/Partitioning/IPartitionService.cs deleted file mode 100644 index 0cbfd64aab..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Partitioning/IPartitionService.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Health.Dicom.Core.Messages.Partitioning; - -namespace Microsoft.Health.Dicom.Core.Features.Partitioning; - -public interface IPartitionService -{ - Task GetOrAddPartitionAsync(string partitionName, CancellationToken cancellationToken = default); - - Task GetPartitionsAsync(CancellationToken cancellationToken = default); - - Task GetPartitionAsync(string partitionName, CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Partitioning/IPartitionStore.cs b/src/Microsoft.Health.Dicom.Core/Features/Partitioning/IPartitionStore.cs deleted file mode 100644 index 228f6a0290..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Partitioning/IPartitionStore.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.Health.Dicom.Core.Features.Partitioning; - -public interface IPartitionStore -{ - Task AddPartitionAsync(string partitionName, CancellationToken cancellationToken = default); - - Task> GetPartitionsAsync(CancellationToken cancellationToken = default); - - Task GetPartitionAsync(string partitionName, CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Partitioning/Partition.cs b/src/Microsoft.Health.Dicom.Core/Features/Partitioning/Partition.cs deleted file mode 100644 index 0e055b395f..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Partitioning/Partition.cs +++ /dev/null @@ -1,39 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Text.Json.Serialization; -using EnsureThat; -using Newtonsoft.Json; - -namespace Microsoft.Health.Dicom.Core.Features.Partitioning; - -public class Partition -{ - public const string DefaultName = "Microsoft.Default"; - - public const string UnknownName = "Unknown"; // use for older binaries where we are unable to get partition name and it is not known - - public const int DefaultKey = 1; - - [JsonPropertyName("partitionKey")] // these explicit names are here for the REST schema - [JsonProperty("partitionKey")] // necessary to not break functions that use the old Newtonsoft.Json - public int Key { get; } - - [JsonPropertyName("partitionName")] // these explicit names are here for the REST schema - [JsonProperty("partitionName")] // necessary to not break functions that use the old Newtonsoft.Json - public string Name { get; } - - public DateTimeOffset CreatedDate { get; set; } - - public static Partition Default { get; } = new(DefaultKey, DefaultName); - - public Partition(int key, string name, DateTimeOffset createdDate = default) - { - Key = key; - Name = EnsureArg.IsNotNull(name, nameof(name)); - CreatedDate = createdDate; - } -} \ No newline at end of file diff --git a/src/Microsoft.Health.Dicom.Core/Features/Partitioning/PartitionCache.cs b/src/Microsoft.Health.Dicom.Core/Features/Partitioning/PartitionCache.cs deleted file mode 100644 index 51ae700346..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Partitioning/PartitionCache.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Features.Common; - -namespace Microsoft.Health.Dicom.Core.Features.Partitioning; - -public class PartitionCache : EphemeralMemoryCache -{ - public PartitionCache(IOptions configuration, ILoggerFactory loggerFactory, ILogger logger) - : base(configuration, loggerFactory, logger) - { - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Partitioning/PartitionService.cs b/src/Microsoft.Health.Dicom.Core/Features/Partitioning/PartitionService.cs deleted file mode 100644 index 20be267fbf..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Partitioning/PartitionService.cs +++ /dev/null @@ -1,81 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.Extensions.Logging; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Validation; -using Microsoft.Health.Dicom.Core.Messages.Partitioning; - -namespace Microsoft.Health.Dicom.Core.Features.Partitioning; - -public class PartitionService : IPartitionService -{ - private readonly IPartitionStore _partitionStore; - private readonly PartitionCache _partitionCache; - private readonly ILogger _logger; - - public PartitionService(PartitionCache partitionCache, IPartitionStore partitionStore, ILogger logger) - { - _partitionStore = EnsureArg.IsNotNull(partitionStore, nameof(partitionStore)); - _partitionCache = EnsureArg.IsNotNull(partitionCache, nameof(partitionCache)); - _logger = EnsureArg.IsNotNull(logger, nameof(logger)); - } - - public async Task GetOrAddPartitionAsync(string partitionName, CancellationToken cancellationToken = default) - { - EnsureArg.IsNotNull(partitionName, nameof(partitionName)); - - _logger.LogInformation("Getting partition with name '{PartitionName}'.", partitionName); - - PartitionNameValidator.Validate(partitionName); - - Partition partition = await _partitionCache.GetAsync(partitionName, partitionName, _partitionStore.GetPartitionAsync, cancellationToken); - - if (partition != null) - { - return new GetOrAddPartitionResponse(partition); - } - - try - { - partition = await _partitionCache.GetAsync(partitionName, partitionName, _partitionStore.AddPartitionAsync, cancellationToken); - return new GetOrAddPartitionResponse(partition); - } - catch (DataPartitionAlreadyExistsException) - { - partition = await _partitionCache.GetAsync(partitionName, partitionName, _partitionStore.GetPartitionAsync, cancellationToken); - return new GetOrAddPartitionResponse(partition); - } - } - - public async Task GetPartitionAsync(string partitionName, CancellationToken cancellationToken = default) - { - EnsureArg.IsNotNull(partitionName, nameof(partitionName)); - - _logger.LogInformation("Getting partition with name '{PartitionName}'.", partitionName); - - PartitionNameValidator.Validate(partitionName); - - Partition partition = await _partitionCache.GetAsync(partitionName, partitionName, _partitionStore.GetPartitionAsync, cancellationToken); - - if (partition == null) - { - throw new DataPartitionsNotFoundException(); - } - - return new GetPartitionResponse(partition); - } - - public async Task GetPartitionsAsync(CancellationToken cancellationToken = default) - { - IEnumerable partitions = await _partitionStore.GetPartitionsAsync(cancellationToken); - return new GetPartitionsResponse(partitions.ToList()); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Query/BaseQueryParameters.cs b/src/Microsoft.Health.Dicom.Core/Features/Query/BaseQueryParameters.cs deleted file mode 100644 index 1f02e7c984..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Query/BaseQueryParameters.cs +++ /dev/null @@ -1,21 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; - -namespace Microsoft.Health.Dicom.Core.Features.Query; - -public class BaseQueryParameters -{ - public IReadOnlyDictionary Filters { get; set; } - - public long Offset { get; set; } - - public int Limit { get; set; } = 100; - - public bool FuzzyMatching { get; set; } - - public IReadOnlyList IncludeField { get; set; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Query/BaseQueryParser.cs b/src/Microsoft.Health.Dicom.Core/Features/Query/BaseQueryParser.cs deleted file mode 100644 index b880110442..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Query/BaseQueryParser.cs +++ /dev/null @@ -1,286 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Net; -using EnsureThat; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Features.Query.Model; - -namespace Microsoft.Health.Dicom.Core.Features.Query; - -/// -/// Value parsers -/// -public abstract class BaseQueryParser : IQueryParser - where TQueryExpression : BaseQueryExpression - where TQueryParameters : BaseQueryParameters -{ - protected const string IncludeFieldValueAll = "all"; - - private readonly static Dictionary> ValueParsers = new Dictionary> - { - { DicomVR.DA, ParseDateOrTimeTagValue }, - { DicomVR.DT, ParseDateOrTimeTagValue }, - { DicomVR.TM, ParseTimeTagValue }, - - { DicomVR.UI, ParseStringTagValue }, - { DicomVR.LO, ParseStringTagValue }, - { DicomVR.SH, ParseStringTagValue }, - { DicomVR.PN, ParseStringTagValue }, - { DicomVR.CS, ParseStringTagValue }, - - { DicomVR.AE, ParseStringTagValue }, - { DicomVR.AS, ParseStringTagValue }, - { DicomVR.DS, ParseStringTagValue }, - { DicomVR.IS, ParseStringTagValue }, - - { DicomVR.SL, ParseLongTagValue }, - { DicomVR.SS, ParseLongTagValue }, - { DicomVR.UL, ParseLongTagValue }, - { DicomVR.US, ParseLongTagValue }, - - { DicomVR.FL, ParseDoubleTagValue }, - { DicomVR.FD, ParseDoubleTagValue }, - }; - - public const string DateTagValueFormat = "yyyyMMdd"; - - public static readonly string[] DateTimeTagValueFormats = - { - "yyyyMMddHHmmss.FFFFFF", - "yyyyMMddHHmmss", - "yyyyMMddHHmm", - "yyyyMMddHH", - "yyyyMMdd", - "yyyyMM", - "yyyy" - }; - - public static readonly string[] DateTimeTagValueWithOffsetFormats = - { - "yyyyMMddHHmmss.FFFFFFzzz", - "yyyyMMddHHmmsszzz", - "yyyyMMddHHmmzzz", - "yyyyMMddHHzzz", - "yyyyMMddzzz", - "yyyyMMzzz", - "yyyyzzz" - }; - - public abstract TQueryExpression Parse(TQueryParameters parameters, IReadOnlyCollection queryTags); - - protected static bool TryGetValueParser(QueryTag tag, bool fuzzyMatching, out Func valueParseFunc) - { - EnsureArg.IsNotNull(tag, nameof(tag)); - valueParseFunc = null; - if (ValueParsers.TryGetValue(tag.VR, out Func valueParser)) - { - valueParseFunc = valueParser; - if (fuzzyMatching && QueryLimit.IsValidFuzzyMatchingQueryTag(tag)) - { - valueParseFunc = ParseFuzzyMatchingTagValue; - } - } - return valueParseFunc != null; - } - - private static PersonNameFuzzyMatchCondition ParseFuzzyMatchingTagValue(QueryTag queryTag, string value) - { - // quotes is not supported in fuzzy matching because of limitation in underlaying implementation - char unspportedChar = '"'; - if (value.Contains(unspportedChar, StringComparison.OrdinalIgnoreCase)) - { - throw new QueryParseException(string.Format( - CultureInfo.CurrentCulture, - DicomCoreResource.InvalidTagValueWithFuzzyMatch, - queryTag.GetName(), - value, - unspportedChar)); - } - - return new PersonNameFuzzyMatchCondition(queryTag, value); - } - - private static QueryFilterCondition ParseDateOrTimeTagValue(QueryTag queryTag, string value) - { - QueryFilterCondition queryFilterCondition = null; - - if (queryTag.VR == DicomVR.DT) - { - queryFilterCondition = ParseDateOrTimeTagValue(queryTag, value, DicomCoreResource.InvalidDateTimeRangeValue, ParseDateTime); - } - else - { - queryFilterCondition = ParseDateOrTimeTagValue(queryTag, value, DicomCoreResource.InvalidDateRangeValue, ParseDate); - } - - return queryFilterCondition; - } - - private static QueryFilterCondition ParseDateOrTimeTagValue(QueryTag queryTag, string value, string exceptionBaseMessage, Func parseValue) - { - if (QueryLimit.IsValidRangeQueryTag(queryTag)) - { - var splitString = value.Split('-'); - if (splitString.Length == 2) - { - string minDateTime = splitString[0].Trim(); - string maxDateTime = splitString[1].Trim(); - - // Make sure both parts of the range values are not empty. - // If so, throw an exception. - ValidateEmptyValuesForRangeQuery(minDateTime, maxDateTime); - - DateTime parsedMinDateTime = string.IsNullOrEmpty(minDateTime) ? DateTime.MinValue : parseValue(minDateTime, queryTag.GetName()); - DateTime parsedMaxDateTime = string.IsNullOrEmpty(maxDateTime) ? DateTime.MaxValue : parseValue(maxDateTime, queryTag.GetName()); - - if (parsedMinDateTime > parsedMaxDateTime) - { - throw new QueryParseException(string.Format( - CultureInfo.CurrentCulture, - exceptionBaseMessage, - value, - minDateTime, - maxDateTime)); - } - - return new DateRangeValueMatchCondition(queryTag, parsedMinDateTime, parsedMaxDateTime); - } - } - - DateTime parsedDateTime = parseValue(value, queryTag.GetName()); - return new DateSingleValueMatchCondition(queryTag, parsedDateTime); - } - - private static QueryFilterCondition ParseTimeTagValue(QueryTag queryTag, string value) - { - if (QueryLimit.IsValidRangeQueryTag(queryTag)) - { - var splitString = value.Split('-'); - if (splitString.Length == 2) - { - string minTime = splitString[0].Trim(); - string maxTime = splitString[1].Trim(); - - // Make sure both parts of the range values are not empty. - // If so, throw an exception. - ValidateEmptyValuesForRangeQuery(minTime, maxTime); - - long parsedMinTime = string.IsNullOrEmpty(minTime) ? 0 : ParseTime(minTime, queryTag); - long parsedMaxTime = string.IsNullOrEmpty(maxTime) ? TimeSpan.TicksPerDay : ParseTime(maxTime, queryTag); - - if (parsedMinTime > parsedMaxTime) - { - throw new QueryParseException(string.Format( - CultureInfo.CurrentCulture, - DicomCoreResource.InvalidTimeRangeValue, - value, - minTime, - maxTime)); - } - - return new LongRangeValueMatchCondition(queryTag, parsedMinTime, parsedMaxTime); - } - } - - long parsedTime = ParseTime(value, queryTag); - return new LongSingleValueMatchCondition(queryTag, parsedTime); - } - - private static QueryFilterCondition ParseStringTagValue(QueryTag queryTag, string value) - { - if (QueryLimit.IsStudyToSeriesTag(queryTag.Tag)) - { - return new StudyToSeriesStringSingleValueMatchCondition(queryTag, value); - } - return new StringSingleValueMatchCondition(queryTag, value); - } - - private static QueryFilterCondition ParseDoubleTagValue(QueryTag queryTag, string value) - { - if (!double.TryParse(value, out double val)) - { - throw new QueryParseException(string.Format(CultureInfo.CurrentCulture, DicomCoreResource.InvalidDoubleValue, value, queryTag.GetName())); - } - - return new DoubleSingleValueMatchCondition(queryTag, val); - } - - private static QueryFilterCondition ParseLongTagValue(QueryTag queryTag, string value) - { - if (!long.TryParse(value, out long val)) - { - throw new QueryParseException(string.Format(CultureInfo.CurrentCulture, DicomCoreResource.InvalidLongValue, value, queryTag.GetName())); - } - - return new LongSingleValueMatchCondition(queryTag, val); - } - - private static DateTime ParseDate(string date, string tagName) - { - if (!DateTime.TryParseExact(date, DateTagValueFormat, null, System.Globalization.DateTimeStyles.None, out DateTime parsedDate)) - { - throw new QueryParseException(string.Format(CultureInfo.CurrentCulture, DicomCoreResource.InvalidDateValue, date, tagName)); - } - - return parsedDate; - } - - private static DateTime ParseDateTime(string dateTime, string tagName) - { - // If the attribute has + in the value (for offset), it is converted to space by default. - // Encoding it to get the + back such that it can be parsed as an offset properly. - string encodedDateTime = WebUtility.UrlEncode(dateTime); - - if (!DateTime.TryParseExact(dateTime, DateTimeTagValueFormats, null, System.Globalization.DateTimeStyles.None, out DateTime parsedDateTime)) - { - if (DateTime.TryParseExact(encodedDateTime, DateTimeTagValueWithOffsetFormats, null, System.Globalization.DateTimeStyles.None, out DateTime parsedDateTimeOffsetNotSupported)) - { - throw new QueryParseException(string.Format(CultureInfo.CurrentCulture, DicomCoreResource.DateTimeWithOffsetNotSupported, encodedDateTime, tagName)); - } - else - { - throw new QueryParseException(string.Format(CultureInfo.CurrentCulture, DicomCoreResource.InvalidDateTimeValue, encodedDateTime, tagName)); - } - } - - return parsedDateTime; - } - - private static long ParseTime(string time, QueryTag queryTag) - { - DateTime timeValue; - - try - { - DicomTime dicomTime = new DicomTime(queryTag.Tag, new string[] { time }); - timeValue = dicomTime.Get(); - } - catch (Exception) - { - throw new QueryParseException(string.Format(CultureInfo.CurrentCulture, DicomCoreResource.InvalidTimeValue, time, queryTag.GetName())); - } - - return timeValue.Ticks; - } - - /// - /// Validate if both values for range query are not empty. - /// If so, throws an exception. - /// - /// Value 1 of range query. - /// Value 2 of range query. - private static void ValidateEmptyValuesForRangeQuery(string value1, string value2) - { - if (string.IsNullOrEmpty(value1) && string.IsNullOrEmpty(value2)) - { - throw new QueryParseException(DicomCoreResource.InvalidRangeValues); - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Query/IQueryParser.cs b/src/Microsoft.Health.Dicom.Core/Features/Query/IQueryParser.cs deleted file mode 100644 index bda6bd946c..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Query/IQueryParser.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Features.Query.Model; - -namespace Microsoft.Health.Dicom.Core.Features.Query; - -public interface IQueryParser - where TQueryExpression : BaseQueryExpression - where TQueryParameters : BaseQueryParameters -{ - TQueryExpression Parse(TQueryParameters parameters, IReadOnlyCollection queryTags); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Query/IQueryService.cs b/src/Microsoft.Health.Dicom.Core/Features/Query/IQueryService.cs deleted file mode 100644 index e6599cf9c8..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Query/IQueryService.cs +++ /dev/null @@ -1,15 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Health.Dicom.Core.Messages.Query; - -namespace Microsoft.Health.Dicom.Core.Features.Query; - -public interface IQueryService -{ - Task QueryAsync(QueryParameters parameters, CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Query/IQueryStore.cs b/src/Microsoft.Health.Dicom.Core/Features/Query/IQueryStore.cs deleted file mode 100644 index ad65174588..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Query/IQueryStore.cs +++ /dev/null @@ -1,29 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Health.Dicom.Core.Features.Query.Model; - -namespace Microsoft.Health.Dicom.Core.Features.Query; - -public interface IQueryStore -{ - Task QueryAsync( - int partitionKey, - QueryExpression query, - CancellationToken cancellationToken = default); - - Task> GetStudyResultAsync( - int partitionKey, - IReadOnlyCollection versions, - CancellationToken cancellationToken = default); - - Task> GetSeriesResultAsync( - int partitionKey, - IReadOnlyCollection versions, - CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Query/Model/BaseQueryExpression.cs b/src/Microsoft.Health.Dicom.Core/Features/Query/Model/BaseQueryExpression.cs deleted file mode 100644 index f6d3dada28..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Query/Model/BaseQueryExpression.cs +++ /dev/null @@ -1,64 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using EnsureThat; - -namespace Microsoft.Health.Dicom.Core.Features.Query.Model; - -/// -/// Valid parsed object representing query parameters for a QIDO-RS request -/// -public class BaseQueryExpression -{ - public BaseQueryExpression( - QueryIncludeField includeFields, - bool fuzzyMatching, - int limit, - long offset, - IReadOnlyCollection filterConditions) - { - IncludeFields = includeFields; - FuzzyMatching = fuzzyMatching; - Limit = limit; - Offset = offset; - FilterConditions = EnsureArg.IsNotNull(filterConditions, nameof(filterConditions)); - } - - /// - /// Dicom tags to include in query result - /// - public QueryIncludeField IncludeFields { get; } - - /// - /// If true do Fuzzy matching of PN tag types - /// - public bool FuzzyMatching { get; } - - /// - /// Query result count - /// - public int Limit { get; } - - /// - /// Query result skip offset count - /// - public long Offset { get; } - - /// - /// List of filter conditions to find the DICOM objects - /// - public IReadOnlyCollection FilterConditions { get; } - - /// - /// Request query was empty - /// - public bool HasFilters => FilterConditions.Count > 0; - - /// - /// evaluted result count for this request - /// - public int EvaluatedLimit => Limit > 0 && Limit <= QueryLimit.MaxQueryResultCount ? Limit : QueryLimit.DefaultQueryResultCount; -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Query/Model/FilterConditions/DateRangeValueMatchCondition.cs b/src/Microsoft.Health.Dicom.Core/Features/Query/Model/FilterConditions/DateRangeValueMatchCondition.cs deleted file mode 100644 index 23388d9d83..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Query/Model/FilterConditions/DateRangeValueMatchCondition.cs +++ /dev/null @@ -1,23 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- -using System; -using EnsureThat; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -namespace Microsoft.Health.Dicom.Core.Features.Query; - -public class DateRangeValueMatchCondition : RangeValueMatchCondition -{ - internal DateRangeValueMatchCondition(QueryTag tag, DateTime minimum, DateTime maximum) - : base(tag, minimum, maximum) - { - } - - public override void Accept(QueryFilterConditionVisitor visitor) - { - EnsureArg.IsNotNull(visitor, nameof(visitor)); - visitor.Visit(this); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Query/Model/FilterConditions/DateSingleValueMatchCondition.cs b/src/Microsoft.Health.Dicom.Core/Features/Query/Model/FilterConditions/DateSingleValueMatchCondition.cs deleted file mode 100644 index 7cfd5ea7a4..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Query/Model/FilterConditions/DateSingleValueMatchCondition.cs +++ /dev/null @@ -1,23 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- -using System; -using EnsureThat; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -namespace Microsoft.Health.Dicom.Core.Features.Query; - -public class DateSingleValueMatchCondition : SingleValueMatchCondition -{ - internal DateSingleValueMatchCondition(QueryTag tag, DateTime value) - : base(tag, value) - { - } - - public override void Accept(QueryFilterConditionVisitor visitor) - { - EnsureArg.IsNotNull(visitor, nameof(visitor)); - visitor.Visit(this); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Query/Model/FilterConditions/DoubleSingleValueMatchCondition.cs b/src/Microsoft.Health.Dicom.Core/Features/Query/Model/FilterConditions/DoubleSingleValueMatchCondition.cs deleted file mode 100644 index d30652f048..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Query/Model/FilterConditions/DoubleSingleValueMatchCondition.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- -using EnsureThat; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -namespace Microsoft.Health.Dicom.Core.Features.Query; - -public class DoubleSingleValueMatchCondition : SingleValueMatchCondition -{ - internal DoubleSingleValueMatchCondition(QueryTag tag, double value) - : base(tag, value) - { - } - - public override void Accept(QueryFilterConditionVisitor visitor) - { - EnsureArg.IsNotNull(visitor, nameof(visitor)); - visitor.Visit(this); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Query/Model/FilterConditions/ExtendedQueryTagFilterDetails.cs b/src/Microsoft.Health.Dicom.Core/Features/Query/Model/FilterConditions/ExtendedQueryTagFilterDetails.cs deleted file mode 100644 index ee39a24e1f..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Query/Model/FilterConditions/ExtendedQueryTagFilterDetails.cs +++ /dev/null @@ -1,62 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -namespace Microsoft.Health.Dicom.Core.Features.Query; - -/// -/// Details required to create a extended query tag filter condition -/// -public class ExtendedQueryTagFilterDetails : IEquatable -{ - public ExtendedQueryTagFilterDetails(int tagKey, QueryTagLevel tagLevel, DicomVR vr, DicomTag tag) - { - Key = tagKey; - VR = vr; - Level = tagLevel; - Tag = tag; - } - - public ExtendedQueryTagFilterDetails(DicomTag tag) - { - Tag = tag; - } - - public int Key { get; } - - public DicomVR VR { get; } - - public QueryTagLevel Level { get; } - - public DicomTag Tag { get; } - - public override string ToString() - { - return $"Key: {Key}, VR {VR.Code} Level:{Level} Tag:{Tag}"; - } - - public override int GetHashCode() - { - return HashCode.Combine(Tag.GetHashCode()); - } - - public override bool Equals(object obj) - { - return Equals(obj as ExtendedQueryTagFilterDetails); - } - - public bool Equals(ExtendedQueryTagFilterDetails other) - { - if (other == null) - { - return false; - } - - return Tag == other.Tag; - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Query/Model/FilterConditions/LongRangeValueMatchCondition.cs b/src/Microsoft.Health.Dicom.Core/Features/Query/Model/FilterConditions/LongRangeValueMatchCondition.cs deleted file mode 100644 index f99bf0ffd8..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Query/Model/FilterConditions/LongRangeValueMatchCondition.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- -using EnsureThat; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -namespace Microsoft.Health.Dicom.Core.Features.Query; - -public class LongRangeValueMatchCondition : RangeValueMatchCondition -{ - internal LongRangeValueMatchCondition(QueryTag tag, long minimum, long maximum) - : base(tag, minimum, maximum) - { - } - - public override void Accept(QueryFilterConditionVisitor visitor) - { - EnsureArg.IsNotNull(visitor, nameof(visitor)); - visitor.Visit(this); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Query/Model/FilterConditions/LongSingleValueMatchCondition.cs b/src/Microsoft.Health.Dicom.Core/Features/Query/Model/FilterConditions/LongSingleValueMatchCondition.cs deleted file mode 100644 index 3a7d53bfef..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Query/Model/FilterConditions/LongSingleValueMatchCondition.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- -using EnsureThat; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -namespace Microsoft.Health.Dicom.Core.Features.Query; - -public class LongSingleValueMatchCondition : SingleValueMatchCondition -{ - internal LongSingleValueMatchCondition(QueryTag tag, long value) - : base(tag, value) - { - } - - public override void Accept(QueryFilterConditionVisitor visitor) - { - EnsureArg.IsNotNull(visitor, nameof(visitor)); - visitor.Visit(this); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Query/Model/FilterConditions/PersonNameFuzzyMatchCondition.cs b/src/Microsoft.Health.Dicom.Core/Features/Query/Model/FilterConditions/PersonNameFuzzyMatchCondition.cs deleted file mode 100644 index b2577268a1..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Query/Model/FilterConditions/PersonNameFuzzyMatchCondition.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- -using EnsureThat; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -namespace Microsoft.Health.Dicom.Core.Features.Query; - -public class PersonNameFuzzyMatchCondition : SingleValueMatchCondition -{ - internal PersonNameFuzzyMatchCondition(QueryTag tag, string value) - : base(tag, value) - { - } - - public override void Accept(QueryFilterConditionVisitor visitor) - { - EnsureArg.IsNotNull(visitor, nameof(visitor)); - visitor.Visit(this); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Query/Model/FilterConditions/QueryFilterCondition.cs b/src/Microsoft.Health.Dicom.Core/Features/Query/Model/FilterConditions/QueryFilterCondition.cs deleted file mode 100644 index f1c35dfb26..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Query/Model/FilterConditions/QueryFilterCondition.cs +++ /dev/null @@ -1,23 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- -using EnsureThat; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -namespace Microsoft.Health.Dicom.Core.Features.Query; - -public abstract class QueryFilterCondition -{ - protected QueryFilterCondition(QueryTag queryTag) - { - EnsureArg.IsNotNull(queryTag, nameof(queryTag)); - QueryTag = queryTag; - } - - - - public QueryTag QueryTag { get; set; } - - public abstract void Accept(QueryFilterConditionVisitor visitor); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Query/Model/FilterConditions/QueryFilterConditionVisitor.cs b/src/Microsoft.Health.Dicom.Core/Features/Query/Model/FilterConditions/QueryFilterConditionVisitor.cs deleted file mode 100644 index 7809ebef0a..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Query/Model/FilterConditions/QueryFilterConditionVisitor.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- -namespace Microsoft.Health.Dicom.Core.Features.Query; - -public abstract class QueryFilterConditionVisitor -{ - public abstract void Visit(StringSingleValueMatchCondition stringSingleValueMatchCondition); - - public abstract void Visit(DateRangeValueMatchCondition rangeValueMatchCondition); - - public abstract void Visit(DateSingleValueMatchCondition dateSingleValueMatchCondition); - - public abstract void Visit(PersonNameFuzzyMatchCondition fuzzyMatchCondition); - - public abstract void Visit(DoubleSingleValueMatchCondition doubleSingleValueMatchCondition); - - public abstract void Visit(LongRangeValueMatchCondition longRangeValueMatchCondition); - - public abstract void Visit(LongSingleValueMatchCondition longSingleValueMatchCondition); - - public abstract void Visit(StudyToSeriesStringSingleValueMatchCondition stringSingleValueMatchCondition); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Query/Model/FilterConditions/RangeValueMatchCondition.cs b/src/Microsoft.Health.Dicom.Core/Features/Query/Model/FilterConditions/RangeValueMatchCondition.cs deleted file mode 100644 index ef31b41c74..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Query/Model/FilterConditions/RangeValueMatchCondition.cs +++ /dev/null @@ -1,21 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -namespace Microsoft.Health.Dicom.Core.Features.Query; - -public abstract class RangeValueMatchCondition : QueryFilterCondition -{ - internal RangeValueMatchCondition(QueryTag tag, T minimum, T maximum) - : base(tag) - { - Minimum = minimum; - Maximum = maximum; - } - - public T Minimum { get; set; } - - public T Maximum { get; set; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Query/Model/FilterConditions/SingleValueMatchCondition.cs b/src/Microsoft.Health.Dicom.Core/Features/Query/Model/FilterConditions/SingleValueMatchCondition.cs deleted file mode 100644 index 9c67c9dbe9..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Query/Model/FilterConditions/SingleValueMatchCondition.cs +++ /dev/null @@ -1,18 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -namespace Microsoft.Health.Dicom.Core.Features.Query; - -public abstract class SingleValueMatchCondition : QueryFilterCondition -{ - internal SingleValueMatchCondition(QueryTag tag, T value) - : base(tag) - { - Value = value; - } - - public T Value { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Query/Model/FilterConditions/StringSingleValueMatchCondition.cs b/src/Microsoft.Health.Dicom.Core/Features/Query/Model/FilterConditions/StringSingleValueMatchCondition.cs deleted file mode 100644 index 2bc4120376..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Query/Model/FilterConditions/StringSingleValueMatchCondition.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- -using EnsureThat; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -namespace Microsoft.Health.Dicom.Core.Features.Query; - -public class StringSingleValueMatchCondition : SingleValueMatchCondition -{ - public StringSingleValueMatchCondition(QueryTag tag, string value) - : base(tag, value) - { - } - - public override void Accept(QueryFilterConditionVisitor visitor) - { - EnsureArg.IsNotNull(visitor, nameof(visitor)); - visitor.Visit(this); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Query/Model/FilterConditions/StudyToSeriesStringSingleValueMatchCondition.cs b/src/Microsoft.Health.Dicom.Core/Features/Query/Model/FilterConditions/StudyToSeriesStringSingleValueMatchCondition.cs deleted file mode 100644 index d496cbac2b..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Query/Model/FilterConditions/StudyToSeriesStringSingleValueMatchCondition.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- -using EnsureThat; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -namespace Microsoft.Health.Dicom.Core.Features.Query; - -public class StudyToSeriesStringSingleValueMatchCondition : SingleValueMatchCondition -{ - public StudyToSeriesStringSingleValueMatchCondition(QueryTag tag, string value) - : base(tag, value) - { - } - - public override void Accept(QueryFilterConditionVisitor visitor) - { - EnsureArg.IsNotNull(visitor, nameof(visitor)); - visitor.Visit(this); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Query/Model/QueryExpression.cs b/src/Microsoft.Health.Dicom.Core/Features/Query/Model/QueryExpression.cs deleted file mode 100644 index f5dcb2963e..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Query/Model/QueryExpression.cs +++ /dev/null @@ -1,80 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using EnsureThat; -using Microsoft.Health.Dicom.Core.Messages; - -namespace Microsoft.Health.Dicom.Core.Features.Query.Model; - -/// -/// Valid parsed object representing query parameters for a QIDO-RS request -/// -public class QueryExpression : BaseQueryExpression -{ - public QueryExpression( - QueryResource resourceType, - QueryIncludeField includeFields, - bool fuzzyMatching, - int limit, - long offset, - IReadOnlyCollection filterConditions, - IReadOnlyCollection erroneousTags) - : base(includeFields, fuzzyMatching, limit, offset, filterConditions) - { - QueryResource = resourceType; - ErroneousTags = EnsureArg.IsNotNull(erroneousTags, nameof(erroneousTags)); - SetIELevel(); - } - - /// - /// Query Resource type level - /// - public QueryResource QueryResource { get; } - - /// - /// Resource level Study/Series - /// - public ResourceType IELevel { get; private set; } - - /// - /// List of erroneous tags. - /// - public IReadOnlyCollection ErroneousTags { get; } - - public bool IsInstanceIELevel() - { - return IELevel == ResourceType.Instance; - } - - public bool IsSeriesIELevel() - { - return IELevel == ResourceType.Series; - } - - public bool IsStudyIELevel() - { - return IELevel == ResourceType.Study; - } - - private void SetIELevel() - { - switch (QueryResource) - { - case QueryResource.AllInstances: - case QueryResource.StudyInstances: - case QueryResource.StudySeriesInstances: - IELevel = ResourceType.Instance; - break; - case QueryResource.AllSeries: - case QueryResource.StudySeries: - IELevel = ResourceType.Series; - break; - case QueryResource.AllStudies: - IELevel = ResourceType.Study; - break; - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Query/Model/QueryIncludeField.cs b/src/Microsoft.Health.Dicom.Core/Features/Query/Model/QueryIncludeField.cs deleted file mode 100644 index 591fc2e104..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Query/Model/QueryIncludeField.cs +++ /dev/null @@ -1,35 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- -using System; -using System.Collections.Generic; -using FellowOakDicom; - -namespace Microsoft.Health.Dicom.Core.Features.Query; - -public class QueryIncludeField -{ - public static QueryIncludeField AllFields { get; } = new QueryIncludeField(true, Array.Empty()); - - public QueryIncludeField(IReadOnlyCollection dicomTags) - : this(false, dicomTags) - { } - - private QueryIncludeField(bool all, IReadOnlyCollection dicomTags) - { - All = all; - DicomTags = dicomTags; - } - - /// - /// If true, include all default and additional fields - /// DicomTags are ignored if all is true - /// - public bool All { get; } - - /// - /// List of additional DicomTags to return with defaults. Used only if "all=false" - /// - public IReadOnlyCollection DicomTags { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Query/Model/QueryResult.cs b/src/Microsoft.Health.Dicom.Core/Features/Query/Model/QueryResult.cs deleted file mode 100644 index 7c07b7d437..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Query/Model/QueryResult.cs +++ /dev/null @@ -1,21 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using EnsureThat; -using Microsoft.Health.Dicom.Core.Features.Model; - -namespace Microsoft.Health.Dicom.Core.Features.Query.Model; - -public class QueryResult -{ - public QueryResult(IEnumerable entries) - { - EnsureArg.IsNotNull(entries, nameof(entries)); - DicomInstances = entries; - } - - public IEnumerable DicomInstances { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Query/Model/SeriesResult.cs b/src/Microsoft.Health.Dicom.Core/Features/Query/Model/SeriesResult.cs deleted file mode 100644 index 7750b476dd..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Query/Model/SeriesResult.cs +++ /dev/null @@ -1,54 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Immutable; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Extensions; - -namespace Microsoft.Health.Dicom.Core.Features.Query.Model; - -public class SeriesResult -{ - public string StudyInstanceUid { get; init; } - public string SeriesInstanceUid { get; init; } - public string Modality { get; init; } - public DateTime? PerformedProcedureStepStartDate { get; init; } - public string ManufacturerModelName { get; init; } - public int NumberOfSeriesRelatedInstances { get; init; } - - private DicomDataset _dicomDataset; - public DicomDataset DicomDataset - { - get - { - if (_dicomDataset == null) - { - _dicomDataset = new DicomDataset() - { - { DicomTag.StudyInstanceUID, StudyInstanceUid }, - { DicomTag.SeriesInstanceUID, SeriesInstanceUid }, - { DicomTag.NumberOfSeriesRelatedInstances, NumberOfSeriesRelatedInstances } - }; - _dicomDataset.AddValueIfNotNull(DicomTag.Modality, Modality); - _dicomDataset.AddValueIfNotNull(DicomTag.ManufacturerModelName, ManufacturerModelName); - if (PerformedProcedureStepStartDate.HasValue) - { - _dicomDataset.Add(DicomTag.PerformedProcedureStepStartDate, PerformedProcedureStepStartDate.Value); - } - } - return _dicomDataset; - } - } - - public static readonly ImmutableHashSet AvailableTags = ImmutableHashSet.Create - ( - DicomTag.SeriesInstanceUID, - DicomTag.Modality, - DicomTag.PerformedProcedureStepStartDate, - DicomTag.ManufacturerModelName, - DicomTag.NumberOfSeriesRelatedInstances - ); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Query/Model/StudyResult.cs b/src/Microsoft.Health.Dicom.Core/Features/Query/Model/StudyResult.cs deleted file mode 100644 index af76d9809e..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Query/Model/StudyResult.cs +++ /dev/null @@ -1,77 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Immutable; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Extensions; - -namespace Microsoft.Health.Dicom.Core.Features.Query.Model; - -public class StudyResult -{ - public string StudyInstanceUid { get; init; } - public string PatientId { get; init; } - public string PatientName { get; init; } - public string ReferringPhysicianName { get; init; } - public DateTime? StudyDate { get; init; } - public string StudyDescription { get; init; } - public string AccessionNumber { get; init; } - public DateTime? PatientBirthDate { get; init; } -#pragma warning disable CA1819 // Properties should not return arrays - public string[] ModalitiesInStudy { get; init; } -#pragma warning restore CA1819 // Properties should not return arrays - public int NumberofStudyRelatedInstances { get; init; } - - private DicomDataset _dicomDataset; - public DicomDataset DicomDataset - { - get - { - if (_dicomDataset == null) - { - _dicomDataset = new DicomDataset() - { - { DicomTag.StudyInstanceUID, StudyInstanceUid }, - { DicomTag.NumberOfStudyRelatedInstances, NumberofStudyRelatedInstances }, - }; - - _dicomDataset.AddValueIfNotNull(DicomTag.PatientID, PatientId); - _dicomDataset.AddValueIfNotNull(DicomTag.PatientName, PatientName); - _dicomDataset.AddValueIfNotNull(DicomTag.ReferringPhysicianName, ReferringPhysicianName); - _dicomDataset.AddValueIfNotNull(DicomTag.StudyDescription, StudyDescription); - _dicomDataset.AddValueIfNotNull(DicomTag.AccessionNumber, AccessionNumber); - - if (ModalitiesInStudy?.Length > 0) - { - _dicomDataset.Add(DicomTag.ModalitiesInStudy, ModalitiesInStudy); - } - if (StudyDate.HasValue) - { - _dicomDataset.Add(DicomTag.StudyDate, StudyDate.Value); - } - if (PatientBirthDate.HasValue) - { - _dicomDataset.Add(DicomTag.PatientBirthDate, PatientBirthDate.Value); - } - } - return _dicomDataset; - } - } - - public static readonly ImmutableHashSet AvailableTags = ImmutableHashSet.Create - ( - DicomTag.StudyInstanceUID, - DicomTag.PatientID, - DicomTag.PatientName, - DicomTag.ReferringPhysicianName, - DicomTag.StudyDate, - DicomTag.StudyDescription, - DicomTag.AccessionNumber, - DicomTag.PatientBirthDate, - DicomTag.ModalitiesInStudy, - DicomTag.NumberOfStudyRelatedInstances - ); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Query/QueryHandler.cs b/src/Microsoft.Health.Dicom.Core/Features/Query/QueryHandler.cs deleted file mode 100644 index 1b661fb46c..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Query/QueryHandler.cs +++ /dev/null @@ -1,39 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using MediatR; -using Microsoft.Health.Core.Features.Security.Authorization; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Security; -using Microsoft.Health.Dicom.Core.Messages.Query; - -namespace Microsoft.Health.Dicom.Core.Features.Query; - -public class QueryHandler : BaseHandler, IRequestHandler -{ - private readonly IQueryService _queryService; - - public QueryHandler(IAuthorizationService authorizationService, IQueryService queryService) - : base(authorizationService) - { - _queryService = EnsureArg.IsNotNull(queryService, nameof(queryService)); - } - - public async Task Handle(QueryResourceRequest request, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(request, nameof(request)); - - if (await AuthorizationService.CheckAccess(DataActions.Read, cancellationToken) != DataActions.Read) - { - throw new UnauthorizedDicomActionException(DataActions.Read); - } - - return await _queryService.QueryAsync(request.Parameters, cancellationToken); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Query/QueryLimit.cs b/src/Microsoft.Health.Dicom.Core/Features/Query/QueryLimit.cs deleted file mode 100644 index 43dde89373..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Query/QueryLimit.cs +++ /dev/null @@ -1,141 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics; -using System.Linq; -using EnsureThat; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Messages; - -namespace Microsoft.Health.Dicom.Core.Features.Query; - -internal static class QueryLimit -{ - public const int MaxQueryResultCount = 200; - public const int DefaultQueryResultCount = 100; - - private static readonly HashSet StudyFilterTags = new HashSet() - { - DicomTag.StudyDate, - DicomTag.StudyInstanceUID, - DicomTag.StudyDescription, - DicomTag.AccessionNumber, - DicomTag.PatientID, - DicomTag.PatientName, - DicomTag.ReferringPhysicianName, - DicomTag.PatientBirthDate, - DicomTag.ModalitiesInStudy, - }; - - private static readonly HashSet SeriesFilterTags = new HashSet() - { - DicomTag.SeriesInstanceUID, - DicomTag.Modality, - DicomTag.PerformedProcedureStepStartDate, - DicomTag.ManufacturerModelName, - }; - - private static readonly HashSet InstanceFilterTags = new HashSet() - { - DicomTag.SOPInstanceUID, - }; - - private static readonly HashSet WorkitemQueryParseTags = new HashSet() - { - DicomTag.RequestedProcedureID, - DicomTag.CodeValue - }; - - private static readonly HashSet StudyResultComputedTags = new HashSet() - { - DicomTag.ModalitiesInStudy, - DicomTag.NumberOfStudyRelatedInstances - }; - - private static readonly HashSet SeriesResultComputedTags = new HashSet() - { - DicomTag.NumberOfSeriesRelatedInstances - }; - - public static readonly HashSet CoreFilterTags = new HashSet( - StudyFilterTags.Union(SeriesFilterTags).Union(InstanceFilterTags)); - - public static readonly HashSet ValidRangeQueryTags = new HashSet() - { - DicomVR.DA, - DicomVR.DT, - DicomVR.TM, - }; - - public static readonly IReadOnlyDictionary> QueryResourceTypeToQueryLevelsMapping = new Dictionary>() - { - { QueryResource.AllStudies, ImmutableHashSet.Create(QueryTagLevel.Study) }, - { QueryResource.AllSeries, ImmutableHashSet.Create(QueryTagLevel.Study, QueryTagLevel.Series) }, - { QueryResource.AllInstances, ImmutableHashSet.Create(QueryTagLevel.Study, QueryTagLevel.Series, QueryTagLevel.Instance) }, - { QueryResource.StudySeries, ImmutableHashSet.Create(QueryTagLevel.Series)}, - { QueryResource.StudyInstances, ImmutableHashSet.Create(QueryTagLevel.Series, QueryTagLevel.Instance) }, - { QueryResource.StudySeriesInstances, ImmutableHashSet.Create(QueryTagLevel.Instance) }, - }; - - /// - /// Get QueryTagLevel of a core tag - /// - /// - /// - public static QueryTagLevel GetQueryTagLevel(DicomTag coreTag) - { - EnsureArg.IsNotNull(coreTag, nameof(coreTag)); - - if (StudyFilterTags.Contains(coreTag)) - { - return QueryTagLevel.Study; - } - if (SeriesFilterTags.Contains(coreTag)) - { - return QueryTagLevel.Series; - } - if (InstanceFilterTags.Contains(coreTag)) - { - return QueryTagLevel.Instance; - } - if (WorkitemQueryParseTags.Contains(coreTag)) - { - return QueryTagLevel.Instance; - } - - Debug.Fail($"{coreTag} is not a core dicom tag"); - return QueryTagLevel.Instance; - } - - public static bool IsValidRangeQueryTag(QueryTag queryTag) - { - EnsureArg.IsNotNull(queryTag, nameof(queryTag)); - return ValidRangeQueryTags.Contains(queryTag.VR); - } - - public static bool IsValidFuzzyMatchingQueryTag(QueryTag queryTag) - { - EnsureArg.IsNotNull(queryTag, nameof(queryTag)); - return queryTag.VR == DicomVR.PN; - } - - public static bool ContainsComputedTag(ResourceType queryTagLevel, IReadOnlyCollection tags) - { - return queryTagLevel switch - { - ResourceType.Study => tags.Any(t => StudyResultComputedTags.Contains(t)), - ResourceType.Series => tags.Any(t => SeriesResultComputedTags.Contains(t)), - _ => false - }; - } - - public static bool IsStudyToSeriesTag(DicomTag tag) - { - return tag == DicomTag.ModalitiesInStudy; - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Query/QueryParameters.cs b/src/Microsoft.Health.Dicom.Core/Features/Query/QueryParameters.cs deleted file mode 100644 index e9cdb096c2..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Query/QueryParameters.cs +++ /dev/null @@ -1,15 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Features.Query; - -public class QueryParameters : BaseQueryParameters -{ - public QueryResource QueryResourceType { get; set; } - - public string StudyInstanceUid { get; set; } - - public string SeriesInstanceUid { get; set; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Query/QueryParseException.cs b/src/Microsoft.Health.Dicom.Core/Features/Query/QueryParseException.cs deleted file mode 100644 index af049c042e..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Query/QueryParseException.cs +++ /dev/null @@ -1,16 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Microsoft.Health.Dicom.Core.Exceptions; - -namespace Microsoft.Health.Dicom.Core.Features.Query; - -public class QueryParseException : ValidationException -{ - public QueryParseException(string message) - : base(message) - { - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Query/QueryParser.cs b/src/Microsoft.Health.Dicom.Core/Features/Query/QueryParser.cs deleted file mode 100644 index fee2369832..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Query/QueryParser.cs +++ /dev/null @@ -1,221 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using EnsureThat; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Features.Query.Model; - -namespace Microsoft.Health.Dicom.Core.Features.Query; - -/// -/// Main parser class that converts uri query parameters to sql ready query expresions for QIDO-RS request -/// -public class QueryParser : BaseQueryParser -{ - private readonly IDicomTagParser _dicomTagPathParser; - - public QueryParser(IDicomTagParser dicomTagPathParser) - => _dicomTagPathParser = EnsureArg.IsNotNull(dicomTagPathParser, nameof(dicomTagPathParser)); - - public override QueryExpression Parse(QueryParameters parameters, IReadOnlyCollection queryTags) - { - EnsureArg.IsNotNull(parameters, nameof(parameters)); - EnsureArg.IsNotNull(queryTags, nameof(queryTags)); - - // Update the list of query tags - queryTags = GetQualifiedQueryTags(queryTags, parameters.QueryResourceType); - - List erroneousTags = []; - - var filterConditions = new Dictionary(); - foreach (KeyValuePair filter in parameters.Filters) - { - // filter conditions with attributeId as key - if (!ParseFilterCondition(filter, queryTags, parameters.FuzzyMatching, out QueryFilterCondition condition)) - { - throw new QueryParseException(string.Format(CultureInfo.CurrentCulture, DicomCoreResource.UnknownQueryParameter, filter.Key)); - } - - if (condition.QueryTag.IsExtendedQueryTag && condition.QueryTag.ExtendedQueryTagStoreEntry.ErrorCount > 0) - { - erroneousTags.Add(filter.Key); - } - - if (!filterConditions.TryAdd(condition.QueryTag.Tag, condition)) - { - throw new QueryParseException(string.Format(CultureInfo.CurrentCulture, DicomCoreResource.DuplicateAttribute, filter.Key)); - } - } - - // add UIDs as filter conditions - AddInstanceUidFilter(parameters.StudyInstanceUid, filterConditions); - AddSeriesUidFilter(parameters.SeriesInstanceUid, filterConditions); - - return new QueryExpression( - parameters.QueryResourceType, - ParseIncludeFields(parameters.IncludeField), - parameters.FuzzyMatching, - parameters.Limit, - parameters.Offset, - filterConditions.Values, - erroneousTags); - } - - /// - /// Adds series instance uid as filter condition when uid not null. - /// - /// Uid received from request parameters. - /// Filter collection to add to. - /// - internal static void AddSeriesUidFilter(string seriesInstanceUid, Dictionary filterConditions) - { - if (seriesInstanceUid != null) - { - var condition = new StringSingleValueMatchCondition(new QueryTag(DicomTag.SeriesInstanceUID), seriesInstanceUid); - if (filterConditions != null && !filterConditions.TryAdd(DicomTag.SeriesInstanceUID, condition)) - { - throw new QueryParseException(DicomCoreResource.DisallowedSeriesInstanceUIDAttribute); - } - } - } - - /// - /// Adds study instance uid as filter condition when uid not null. - /// - /// Uid received from request parameters. - /// Filter collection to add to. - /// - internal static void AddInstanceUidFilter(string studyInstanceUid, Dictionary filterConditions) - { - if (studyInstanceUid != null) - { - var condition = new StringSingleValueMatchCondition(new QueryTag(DicomTag.StudyInstanceUID), studyInstanceUid); - if (filterConditions != null && !filterConditions.TryAdd(DicomTag.StudyInstanceUID, condition)) - { - throw new QueryParseException(DicomCoreResource.DisallowedStudyInstanceUIDAttribute); - } - } - } - - private static List GetQualifiedQueryTags(IReadOnlyCollection queryTags, QueryResource queryResource) - { - return queryTags.Where(tag => - { - // extended query tag need to Ready to be used. - if (tag.IsExtendedQueryTag && tag.ExtendedQueryTagStoreEntry.Status != ExtendedQueryTagStatus.Ready) - { - return false; - } - - // tag level should be qualified - return QueryLimit.QueryResourceTypeToQueryLevelsMapping[queryResource].Contains(tag.Level); - - }).ToList(); - } - - private bool ParseFilterCondition( - KeyValuePair queryParameter, - IEnumerable queryTags, - bool fuzzyMatching, - out QueryFilterCondition condition) - { - condition = null; - - // parse tag - if (!TryParseDicomAttributeId(queryParameter.Key, out DicomTag dicomTag)) - { - return false; - } - - // QueryTag could be either core or extended query tag or workitem query tag. - QueryTag queryTag = GetMatchingQueryTag(dicomTag, queryParameter.Key, queryTags); - - // check if tag is disabled - if (queryTag.IsExtendedQueryTag && queryTag.ExtendedQueryTagStoreEntry.QueryStatus == QueryStatus.Disabled) - { - throw new QueryParseException(string.Format(CultureInfo.CurrentCulture, DicomCoreResource.QueryIsDisabledOnAttribute, queryParameter.Key)); - } - - if (string.IsNullOrWhiteSpace(queryParameter.Value)) - { - throw new QueryParseException(string.Format(CultureInfo.CurrentCulture, DicomCoreResource.QueryEmptyAttributeValue, queryParameter.Key)); - } - - if (!TryGetValueParser(queryTag, fuzzyMatching, out Func valueParser)) - { - return false; - } - - condition = valueParser(queryTag, queryParameter.Value); - return true; - } - - private bool TryParseDicomAttributeId(string attributeId, out DicomTag dicomTag) - { - if (_dicomTagPathParser.TryParse(attributeId, out DicomTag[] result)) - { - dicomTag = result[0]; - return true; - } - - dicomTag = null; - return false; - } - - private static QueryTag GetMatchingQueryTag(DicomTag dicomTag, string attributeId, IEnumerable queryTags) - { - QueryTag queryTag = queryTags.FirstOrDefault(item => - { - // private tag from request doesn't have private creator, should do path comparison. - if (dicomTag.IsPrivate) - { - return item.Tag.GetPath() == dicomTag.GetPath(); - } - - return item.Tag == dicomTag; - }); - - if (queryTag == null) - { - throw new QueryParseException(string.Format(CultureInfo.CurrentCulture, DicomCoreResource.UnsupportedSearchParameter, attributeId)); - } - - return queryTag; - } - - private QueryIncludeField ParseIncludeFields(IReadOnlyList includeFields) - { - // Check if "all" is present as one of the values in IncludeField parameter. - if (includeFields.Any(val => IncludeFieldValueAll.Equals(val, StringComparison.OrdinalIgnoreCase))) - { - if (includeFields.Count > 1) - { - throw new QueryParseException(DicomCoreResource.InvalidIncludeAllFields); - } - - return QueryIncludeField.AllFields; - } - - var fields = new List(includeFields.Count); - foreach (string field in includeFields) - { - if (!TryParseDicomAttributeId(field, out DicomTag dicomTag)) - { - throw new QueryParseException(string.Format(CultureInfo.InvariantCulture, DicomCoreResource.IncludeFieldUnknownAttribute, field)); - } - - fields.Add(dicomTag); - } - - return new QueryIncludeField(fields); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Query/QueryResource.cs b/src/Microsoft.Health.Dicom.Core/Features/Query/QueryResource.cs deleted file mode 100644 index 3762f34fd1..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Query/QueryResource.cs +++ /dev/null @@ -1,20 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Features.Query; - -public enum QueryResource -{ - // The order of these enum values is important as it allows for a simple way to determine what the minimum resource level requested is. - // It is currently used to identify that a given query parameter is not supported for a resource level. - // For example, Study Series is looking for series and therefore an instance tag would be invalid. - // 0 -> Studies, 1 & 2 -> Series, >= 3 -> Instances. - AllStudies, - AllSeries, - StudySeries, - AllInstances, - StudyInstances, - StudySeriesInstances, -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Query/QueryResponseBuilder.cs b/src/Microsoft.Health.Dicom.Core/Features/Query/QueryResponseBuilder.cs deleted file mode 100644 index ea18b33164..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Query/QueryResponseBuilder.cs +++ /dev/null @@ -1,177 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics; -using System.Linq; -using EnsureThat; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Features.Query.Model; -using Microsoft.Health.Dicom.Core.Messages; - -namespace Microsoft.Health.Dicom.Core.Features.Query; - -internal class QueryResponseBuilder -{ - internal static readonly ImmutableHashSet DefaultStudyTags = ImmutableHashSet.CreateRange(new DicomTag[] - { - DicomTag.SpecificCharacterSet, - DicomTag.StudyDate, - DicomTag.StudyTime, - DicomTag.AccessionNumber, - DicomTag.InstanceAvailability, - DicomTag.ReferringPhysicianName, - DicomTag.TimezoneOffsetFromUTC, - DicomTag.PatientName, - DicomTag.PatientID, - DicomTag.PatientBirthDate, - DicomTag.PatientSex, - DicomTag.StudyInstanceUID, - DicomTag.StudyID, - }); - - internal static readonly ImmutableHashSet V2DefaultStudyTags = ImmutableHashSet.CreateRange(new DicomTag[] - { - DicomTag.StudyInstanceUID, - DicomTag.StudyDate, - DicomTag.StudyDescription, - DicomTag.AccessionNumber, - DicomTag.ReferringPhysicianName, - DicomTag.PatientName, - DicomTag.PatientID, - DicomTag.PatientBirthDate - }); - - internal static readonly ImmutableHashSet AllStudyTags = DefaultStudyTags.Union(new DicomTag[] - { - DicomTag.StudyDescription, - DicomTag.AnatomicRegionsInStudyCodeSequence, - DicomTag.ProcedureCodeSequence, - DicomTag.NameOfPhysiciansReadingStudy, - DicomTag.AdmittingDiagnosesDescription, - DicomTag.ReferencedStudySequence, - DicomTag.PatientAge, - DicomTag.PatientSize, - DicomTag.PatientWeight, - DicomTag.Occupation, - DicomTag.AdditionalPatientHistory, - }); - - internal static readonly ImmutableHashSet DefaultSeriesTags = ImmutableHashSet.CreateRange(new DicomTag[] - { - DicomTag.SpecificCharacterSet, - DicomTag.Modality, - DicomTag.TimezoneOffsetFromUTC, - DicomTag.SeriesDescription, - DicomTag.SeriesInstanceUID, - DicomTag.PerformedProcedureStepStartDate, - DicomTag.PerformedProcedureStepStartTime, - DicomTag.RequestAttributesSequence, - }); - - - internal static readonly ImmutableHashSet V2DefaultSeriesTags = ImmutableHashSet.CreateRange(new DicomTag[] - { - DicomTag.SeriesInstanceUID, - DicomTag.Modality, - DicomTag.PerformedProcedureStepStartDate, - DicomTag.ManufacturerModelName - }); - - internal static readonly ImmutableHashSet AllSeriesTags = DefaultSeriesTags.Union(new DicomTag[] - { - DicomTag.SeriesNumber, - DicomTag.Laterality, - DicomTag.SeriesDate, - DicomTag.SeriesTime, - }); - - internal static readonly ImmutableHashSet DefaultInstancesTags = ImmutableHashSet.CreateRange(new DicomTag[] - { - DicomTag.SpecificCharacterSet, - DicomTag.SOPClassUID, - DicomTag.SOPInstanceUID, - DicomTag.InstanceAvailability, - DicomTag.TimezoneOffsetFromUTC, - DicomTag.InstanceNumber, - DicomTag.Rows, - DicomTag.Columns, - DicomTag.BitsAllocated, - DicomTag.NumberOfFrames, - }); - - internal static readonly ImmutableHashSet V2DefaultInstancesTags = ImmutableHashSet.CreateRange(new DicomTag[] - { - DicomTag.SOPInstanceUID - }); - - private static readonly ImmutableHashSet AllInstancesTags = DefaultInstancesTags; - - private HashSet _tagsToReturn; - - public QueryResponseBuilder(QueryExpression queryExpression, bool useNewDefaults = false) - { - EnsureArg.IsNotNull(queryExpression, nameof(queryExpression)); - EnsureArg.IsFalse(queryExpression.IELevel == ResourceType.Frames, nameof(queryExpression.IELevel)); - - Initialize(queryExpression, useNewDefaults); - } - - public DicomDataset GenerateResponseDataset(DicomDataset dicomDataset) - { - EnsureArg.IsNotNull(dicomDataset, nameof(dicomDataset)); - - dicomDataset.Remove(di => !_tagsToReturn.Any( - t => t.Group == di.Tag.Group && - t.Element == di.Tag.Element)); - - return dicomDataset; - } - - public IReadOnlyCollection ReturnTags => _tagsToReturn; - - // If the target resource is All Series, then Study level attributes are also returned. - // If the target resource is All Instances, then Study and Series level attributes are also returned. - // If the target resource is Study's Instances, then Series level attributes are also returned. - private void Initialize(QueryExpression queryExpression, bool useNewDefaults) - { - switch (queryExpression.QueryResource) - { - case QueryResource.AllStudies: - _tagsToReturn = new HashSet(queryExpression.IncludeFields.All ? AllStudyTags : useNewDefaults ? V2DefaultStudyTags : DefaultStudyTags); - break; - case QueryResource.AllSeries: - _tagsToReturn = new HashSet(queryExpression.IncludeFields.All ? AllStudyTags.Union(AllSeriesTags) : useNewDefaults ? V2DefaultStudyTags.Union(V2DefaultSeriesTags) : DefaultStudyTags.Union(DefaultSeriesTags)); - break; - case QueryResource.AllInstances: - _tagsToReturn = new HashSet(queryExpression.IncludeFields.All ? AllStudyTags.Union(AllSeriesTags).Union(AllInstancesTags) : useNewDefaults ? V2DefaultStudyTags.Union(V2DefaultSeriesTags).Union(V2DefaultInstancesTags) : DefaultStudyTags.Union(DefaultSeriesTags).Union(DefaultInstancesTags)); - break; - case QueryResource.StudySeries: - _tagsToReturn = new HashSet(queryExpression.IncludeFields.All ? AllSeriesTags : useNewDefaults ? V2DefaultSeriesTags : DefaultSeriesTags); - break; - case QueryResource.StudyInstances: - _tagsToReturn = new HashSet(queryExpression.IncludeFields.All ? AllSeriesTags.Union(AllInstancesTags) : useNewDefaults ? V2DefaultSeriesTags.Union(V2DefaultInstancesTags) : DefaultSeriesTags.Union(DefaultInstancesTags)); - break; - case QueryResource.StudySeriesInstances: - _tagsToReturn = new HashSet(queryExpression.IncludeFields.All ? AllInstancesTags : useNewDefaults ? V2DefaultInstancesTags : DefaultInstancesTags); - break; - default: - Debug.Fail("A newly added queryResource is not implemeted here"); - break; - } - - foreach (DicomTag tag in queryExpression.IncludeFields.DicomTags) - { - // we will allow any valid include tag. This will allow customers to get any extended query tags in resposne. - _tagsToReturn.Add(tag); - } - - foreach (var cond in queryExpression.FilterConditions) - { - _tagsToReturn.Add(cond.QueryTag.Tag); - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Query/QueryService.cs b/src/Microsoft.Health.Dicom.Core/Features/Query/QueryService.cs deleted file mode 100644 index 85839b92e3..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Query/QueryService.cs +++ /dev/null @@ -1,211 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Data; -using System.Diagnostics; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using FellowOakDicom; -using Microsoft.Extensions.Logging; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Context; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Features.Query.Model; -using Microsoft.Health.Dicom.Core.Features.Validation; -using Microsoft.Health.Dicom.Core.Messages.Query; -using Microsoft.Health.Dicom.Core.Models.Common; - -namespace Microsoft.Health.Dicom.Core.Features.Query; - -public class QueryService : IQueryService -{ - private readonly IQueryParser _queryParser; - private readonly IQueryStore _queryStore; - private readonly IMetadataStore _metadataStore; - private readonly IQueryTagService _queryTagService; - private readonly IDicomRequestContextAccessor _contextAccessor; - private readonly ILogger _logger; - - public QueryService( - IQueryParser queryParser, - IQueryStore queryStore, - IMetadataStore metadataStore, - IQueryTagService queryTagService, - IDicomRequestContextAccessor contextAccessor, - ILogger logger) - { - _queryParser = EnsureArg.IsNotNull(queryParser, nameof(queryParser)); - _queryStore = EnsureArg.IsNotNull(queryStore, nameof(queryStore)); - _metadataStore = EnsureArg.IsNotNull(metadataStore, nameof(metadataStore)); - _queryTagService = EnsureArg.IsNotNull(queryTagService, nameof(queryTagService)); - _contextAccessor = EnsureArg.IsNotNull(contextAccessor, nameof(contextAccessor)); - _logger = EnsureArg.IsNotNull(logger, nameof(logger)); - } - - public async Task QueryAsync( - QueryParameters parameters, - CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(parameters); - - ValidateRequestIdentifiers(parameters); - - var queryTags = await _queryTagService.GetQueryTagsAsync(cancellationToken: cancellationToken); - - QueryExpression queryExpression = _queryParser.Parse(parameters, queryTags); - - var partitionKey = _contextAccessor.RequestContext.GetPartitionKey(); - - Stopwatch stopwatch = new Stopwatch(); - - stopwatch.Start(); - QueryResult queryResult = await _queryStore.QueryAsync(partitionKey, queryExpression, cancellationToken); - stopwatch.Stop(); - var filterTime = stopwatch.ElapsedMilliseconds; - - if (!queryResult.DicomInstances.Any()) - { - return new QueryResourceResponse(Array.Empty(), queryExpression.ErroneousTags); - } - - var responseBuilder = new QueryResponseBuilder(queryExpression, ReturnNewTagDefaults(_contextAccessor.RequestContext.Version)); - - stopwatch.Restart(); - IEnumerable instanceMetadata = await GetInstanceMetadataAsync(partitionKey, queryExpression, queryResult, responseBuilder.ReturnTags, cancellationToken); - stopwatch.Stop(); - var resultTime = stopwatch.ElapsedMilliseconds; - _logger.LogInformation("QueryService performance filterTimeMilliseconds:{FilterTime}, resultTimeMilliseconds:{ResultTime}", filterTime, resultTime); - - var responseMetadata = instanceMetadata.Select(m => responseBuilder.GenerateResponseDataset(m)); - return new QueryResourceResponse(responseMetadata, queryExpression.ErroneousTags); - } - - private static bool ReturnNewTagDefaults(int? version) - { - bool useNewDefaults = false; - if (version != null && version >= 2) - { - useNewDefaults = true; - } - return useNewDefaults; - } - - // Does not handle retrieving the extendedQueryTag indexes right now. Logs are in place to evaluate it in the future. - private async Task> GetInstanceMetadataAsync( - int partitionKey, - QueryExpression queryExpression, - QueryResult queryResult, - IReadOnlyCollection returnTags, - CancellationToken cancellationToken) - { - bool getStudyResponse = false, getSeriesResponse = false, getFullMetadata = false; - - ImmutableHashSet tags = returnTags.ToImmutableHashSet(); - ImmutableHashSet remaining = tags.Except( - StudyResult.AvailableTags.Union(SeriesResult.AvailableTags)); - - if (remaining.Count > 0) - { - getFullMetadata = true; - if (QueryLimit.ContainsComputedTag(queryExpression.IELevel, returnTags)) - { - if (queryExpression.IELevel == Messages.ResourceType.Study) - getStudyResponse = true; - else if (queryExpression.IELevel == Messages.ResourceType.Series) - getSeriesResponse = true; - } - } - else - { - getStudyResponse = tags.Overlaps(StudyResult.AvailableTags); - getSeriesResponse = tags.Overlaps(SeriesResult.AvailableTags); - } - - // logging to track usage - _logger.LogInformation("QueryService result retrieval resultCount:{ResultCount}, studyResultRetrieved:{StudyResultRetrieved}, seriesResultRetrieved:{SeriesResultRetrieved}, fullMetadataRetrieved:{FullMetadataRetrieved}", queryResult.DicomInstances.Count(), getStudyResponse, getSeriesResponse, getFullMetadata); - - // start getting and merging the results based on the source. - IEnumerable instanceMetadata = null; - List versions = queryResult.DicomInstances.Select(i => i.Version).ToList(); - if (getFullMetadata) - { - instanceMetadata = await Task.WhenAll( - queryResult.DicomInstances.Select(x => _metadataStore.GetInstanceMetadataAsync(x.Version, cancellationToken)).ToList()); - } - if (getSeriesResponse) - { - IReadOnlyCollection seriesComputedResults = await _queryStore.GetSeriesResultAsync(partitionKey, versions, cancellationToken); - - if (instanceMetadata == null) - { - instanceMetadata = seriesComputedResults.Select(x => x.DicomDataset).ToList(); - } - else - { - Dictionary map = seriesComputedResults.ToDictionary(a => new DicomIdentifier(a.StudyInstanceUid, a.SeriesInstanceUid, default)); - - // Added 'where' to filter the dataset if the particular StudyInstanceUID is not present in the computed list from database. - // This is to handle the edge case where the map doesn't have the StudyInstanceUID received from blob store. - instanceMetadata = instanceMetadata.Where(x => - map.ContainsKey(new DicomIdentifier(x.GetSingleValue(DicomTag.StudyInstanceUID), x.GetSingleValue(DicomTag.SeriesInstanceUID), default))).Select(x => - { - var ds = new DicomDataset(x); - return ds.AddOrUpdate(map[new DicomIdentifier(x.GetSingleValue(DicomTag.StudyInstanceUID), x.GetSingleValue(DicomTag.SeriesInstanceUID), default)].DicomDataset); - }); - } - } - if (getStudyResponse) - { - IReadOnlyCollection studyComputedResults = await _queryStore.GetStudyResultAsync(partitionKey, versions, cancellationToken); - if (instanceMetadata == null) - { - instanceMetadata = studyComputedResults.Select(x => x.DicomDataset).ToList(); - } - else - { - Dictionary map = studyComputedResults.ToDictionary(a => a.StudyInstanceUid, StringComparer.OrdinalIgnoreCase); - - // Added 'where' to filter the dataset if the particular StudyInstanceUID is not present in the computed list from database. - // This is to handle the edge case where the map doesn't have the StudyInstanceUID received from blob store. - instanceMetadata = instanceMetadata.Where(x => - map.ContainsKey(x.GetSingleValue(DicomTag.StudyInstanceUID))).Select(x => - { - var ds = new DicomDataset(x); - return ds.AddOrUpdate(map[x.GetSingleValue(DicomTag.StudyInstanceUID)].DicomDataset); - }); - } - } - - return instanceMetadata; - } - - private static void ValidateRequestIdentifiers(QueryParameters parameters) - { - switch (parameters.QueryResourceType) - { - case QueryResource.StudySeries: - case QueryResource.StudyInstances: - UidValidation.Validate(parameters.StudyInstanceUid, nameof(parameters.StudyInstanceUid)); - break; - case QueryResource.StudySeriesInstances: - UidValidation.Validate(parameters.StudyInstanceUid, nameof(parameters.StudyInstanceUid)); - UidValidation.Validate(parameters.SeriesInstanceUid, nameof(parameters.SeriesInstanceUid)); - break; - case QueryResource.AllStudies: - case QueryResource.AllSeries: - case QueryResource.AllInstances: - break; - default: - Debug.Fail("A newly added query resource is not handled."); - break; - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/AcceptHeaderDescriptor.cs b/src/Microsoft.Health.Dicom.Core/Features/Retrieve/AcceptHeaderDescriptor.cs deleted file mode 100644 index 04c45046c4..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/AcceptHeaderDescriptor.cs +++ /dev/null @@ -1,95 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using EnsureThat; -using Microsoft.Extensions.Primitives; -using Microsoft.Health.Dicom.Core.Messages.Retrieve; - -namespace Microsoft.Health.Dicom.Core.Features.Retrieve; - -public class AcceptHeaderDescriptor -{ - public AcceptHeaderDescriptor( - PayloadTypes payloadType, - string mediaType, - bool isTransferSyntaxMandatory, - string transferSyntaxWhenMissing, - ISet acceptableTransferSyntaxes - ) - { - EnsureArg.IsNotEmptyOrWhiteSpace(mediaType, nameof(mediaType)); - - // When transfersyntax is not mandatory, transferSyntaxWhenMissing has to be presented - if (!isTransferSyntaxMandatory) - { - EnsureArg.IsNotEmptyOrWhiteSpace(transferSyntaxWhenMissing, nameof(transferSyntaxWhenMissing)); - } - - EnsureArg.IsNotNull(acceptableTransferSyntaxes, nameof(acceptableTransferSyntaxes)); - - PayloadType = payloadType; - MediaType = mediaType; - IsTransferSyntaxMandatory = isTransferSyntaxMandatory; - TransferSyntaxWhenMissing = transferSyntaxWhenMissing; - AcceptableTransferSyntaxes = acceptableTransferSyntaxes; - } - - public PayloadTypes PayloadType { get; } - - public string MediaType { get; } - - public bool IsTransferSyntaxMandatory { get; } - - public string TransferSyntaxWhenMissing { get; } - - public ISet AcceptableTransferSyntaxes { get; } - - public bool IsAcceptable(AcceptHeader acceptHeader) - { - EnsureArg.IsNotNull(acceptHeader, nameof(acceptHeader)); - - // Check if payload type match - if ((PayloadType & acceptHeader.PayloadType) == PayloadTypes.None) - { - return false; - } - - if (!StringSegment.Equals(acceptHeader.MediaType, MediaType, StringComparison.InvariantCultureIgnoreCase)) - { - return false; - } - - if (StringSegment.IsNullOrEmpty(acceptHeader.TransferSyntax)) - { - if (IsTransferSyntaxMandatory) - { - return false; - } - return true; - } - - if (AcceptableTransferSyntaxes.Contains(acceptHeader.TransferSyntax.Value)) - { - return true; - } - - return false; - } - - public StringSegment GetTransferSyntax(AcceptHeader acceptHeader) - { - EnsureArg.IsNotNull(acceptHeader, nameof(acceptHeader)); - - // when transfer syntax not supplied and was not mandatory to be supplied, use default syntax - if (!IsTransferSyntaxMandatory && StringSegment.IsNullOrEmpty(acceptHeader.TransferSyntax)) - { - return TransferSyntaxWhenMissing; - } - - return acceptHeader.TransferSyntax; - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/AcceptHeaderHandler.cs b/src/Microsoft.Health.Dicom.Core/Features/Retrieve/AcceptHeaderHandler.cs deleted file mode 100644 index 94bba113b8..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/AcceptHeaderHandler.cs +++ /dev/null @@ -1,197 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using EnsureThat; -using FellowOakDicom; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Primitives; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Messages; -using Microsoft.Health.Dicom.Core.Messages.Retrieve; -using Microsoft.Health.Dicom.Core.Web; - -namespace Microsoft.Health.Dicom.Core.Features.Retrieve; - -public class AcceptHeaderHandler : IAcceptHeaderHandler -{ - protected internal static readonly IReadOnlyDictionary> - AcceptableDescriptors = - new Dictionary>() - { - { ResourceType.Study, DescriptorsForGetNonFrameResource(PayloadTypes.MultipartRelated) }, - { ResourceType.Series, DescriptorsForGetNonFrameResource(PayloadTypes.MultipartRelated) }, - { ResourceType.Instance, DescriptorsForGetNonFrameResource(PayloadTypes.SinglePartOrMultipartRelated) }, - { ResourceType.Frames, DescriptorsForGetFrame() }, - }; - - private readonly IReadOnlyDictionary> _acceptableDescriptors; - - private readonly ILogger _logger; - - public AcceptHeaderHandler(ILogger logger) - : this(AcceptableDescriptors, logger) - { - } - - private AcceptHeaderHandler( - IReadOnlyDictionary> acceptableDescriptors, - ILogger logger) - { - EnsureArg.IsNotNull(logger, nameof(logger)); - EnsureArg.IsNotNull(acceptableDescriptors, nameof(acceptableDescriptors)); - - _acceptableDescriptors = acceptableDescriptors; - _logger = logger; - } - - /// - /// Based on requested AcceptHeaders from users ordered by priority, create new AcceptHeader with valid - /// TransferSyntax, leaving user input unmodified. - /// - /// Used to understand if header properties are valid. - /// One or more headers as requested by user. - /// New accept header based on highest priority valid header requested. - /// - public AcceptHeader GetValidAcceptHeader(ResourceType resourceType, IReadOnlyCollection acceptHeaders) - { - EnsureArg.IsNotNull(acceptHeaders, nameof(acceptHeaders)); - List orderedHeaders = acceptHeaders.OrderByDescending(x => x.Quality ?? AcceptHeader.DefaultQuality).ToList(); - - _logger.LogInformation( - "Getting transfer syntax for retrieving {ResourceType} with accept headers {AcceptHeaders}.", - resourceType, - string.Join(";", orderedHeaders)); - - List descriptors = _acceptableDescriptors[resourceType]; - - AcceptHeader selectedHeader = null; - // we will return the highest priority media type we support - foreach (AcceptHeader header in orderedHeaders) - { - foreach (AcceptHeaderDescriptor descriptor in descriptors) - { - if (descriptor.IsAcceptable(header) && (selectedHeader == null || IsHigherPriorityTransferSyntax(header, selectedHeader))) - { - selectedHeader = new AcceptHeader( - GetMediaTypesString(header.MediaType, resourceType), - GetPayloadType(header), - descriptor.GetTransferSyntax(header), - header.Quality); - - continue; - } - } - } - - if (selectedHeader != null) - { - _logger.LogInformation("Selected accept header {AcceptHeader} for retrieving {ResourceType}.", selectedHeader, resourceType); - return selectedHeader; - } - - // none were valid - throw new NotAcceptableException(DicomCoreResource.NotAcceptableHeaders); - } - - // if no quality provided prioritize returning original transfer syntax - private static bool IsHigherPriorityTransferSyntax(AcceptHeader header, AcceptHeader selectedHeader) - { - bool isQualityGreater = (header.Quality ?? AcceptHeader.DefaultQuality) >= (selectedHeader.Quality ?? AcceptHeader.DefaultQuality); - return (header.TransferSyntax.Value == DicomTransferSyntaxUids.Original && isQualityGreater); - } - - private static PayloadTypes GetPayloadType(AcceptHeader header) - { - if (header.MediaType != KnownContentTypes.AnyMediaType) - { - return header.PayloadType; - } - - return PayloadTypes.MultipartRelated; - } - - // If the media type is */* then we need to return the default media type for the resource type - private static StringSegment GetMediaTypesString(StringSegment mediaType, ResourceType resourceType) - { - if (mediaType != KnownContentTypes.AnyMediaType) - { - return mediaType; - } - - if (resourceType == ResourceType.Frames) - { - return KnownContentTypes.ApplicationOctetStream; - } - - return KnownContentTypes.ApplicationDicom; - } - - private static List DescriptorsForGetNonFrameResource(PayloadTypes payloadTypes) - { - return new List - { - new AcceptHeaderDescriptor( - payloadType: payloadTypes, - mediaType: KnownContentTypes.ApplicationDicom, - isTransferSyntaxMandatory: false, - transferSyntaxWhenMissing: DicomTransferSyntax.ExplicitVRLittleEndian.UID.UID, - acceptableTransferSyntaxes: GetAcceptableTransferSyntaxSet( - DicomTransferSyntaxUids.Original, - DicomTransferSyntax.ExplicitVRLittleEndian.UID.UID, - DicomTransferSyntax.JPEG2000Lossless.UID.UID)), - new AcceptHeaderDescriptor( - payloadType: payloadTypes, - mediaType: KnownContentTypes.AnyMediaType, - isTransferSyntaxMandatory: false, - transferSyntaxWhenMissing: DicomTransferSyntaxUids.Original, - acceptableTransferSyntaxes: GetAcceptableTransferSyntaxSet( - DicomTransferSyntaxUids.Original, - DicomTransferSyntax.ExplicitVRLittleEndian.UID.UID, - DicomTransferSyntax.JPEG2000Lossless.UID.UID)) - }; - } - - private static List DescriptorsForGetFrame() - { - return new List - { - new AcceptHeaderDescriptor( - payloadType: PayloadTypes.SinglePartOrMultipartRelated, - mediaType: KnownContentTypes.ApplicationOctetStream, - isTransferSyntaxMandatory: false, - transferSyntaxWhenMissing: DicomTransferSyntax.ExplicitVRLittleEndian.UID.UID, - acceptableTransferSyntaxes: GetAcceptableTransferSyntaxSet( - DicomTransferSyntaxUids.Original, - DicomTransferSyntax.ExplicitVRLittleEndian.UID.UID)), - new AcceptHeaderDescriptor( - payloadType: PayloadTypes.MultipartRelated, - mediaType: KnownContentTypes.ImageJpeg2000, - isTransferSyntaxMandatory: false, - transferSyntaxWhenMissing: DicomTransferSyntax.JPEG2000Lossless.UID.UID, - acceptableTransferSyntaxes: GetAcceptableTransferSyntaxSet(DicomTransferSyntax.JPEG2000Lossless)), - new AcceptHeaderDescriptor( - payloadType: PayloadTypes.SinglePartOrMultipartRelated, - mediaType: KnownContentTypes.AnyMediaType, - isTransferSyntaxMandatory: false, - transferSyntaxWhenMissing: DicomTransferSyntaxUids.Original, - acceptableTransferSyntaxes: GetAcceptableTransferSyntaxSet( - DicomTransferSyntaxUids.Original, - DicomTransferSyntax.ExplicitVRLittleEndian.UID.UID)), - }; - } - - private static HashSet GetAcceptableTransferSyntaxSet(params DicomTransferSyntax[] transferSyntaxes) - { - return GetAcceptableTransferSyntaxSet(transferSyntaxes.Select(item => item.UID.UID).ToArray()); - } - - private static HashSet GetAcceptableTransferSyntaxSet(params string[] transferSyntaxes) - { - return new HashSet(transferSyntaxes, StringComparer.InvariantCultureIgnoreCase); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/DicomDatasetExtensions.cs b/src/Microsoft.Health.Dicom.Core/Features/Retrieve/DicomDatasetExtensions.cs deleted file mode 100644 index 46f1723db1..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/DicomDatasetExtensions.cs +++ /dev/null @@ -1,122 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using EnsureThat; -using FellowOakDicom; -using FellowOakDicom.Imaging; - -namespace Microsoft.Health.Dicom.Core.Features.Retrieve; - -public static class DicomDatasetExtensions -{ - private static readonly HashSet SupportedTransferSyntaxes8Bit = new HashSet - { - DicomTransferSyntax.DeflatedExplicitVRLittleEndian, - DicomTransferSyntax.ExplicitVRBigEndian, - DicomTransferSyntax.ExplicitVRLittleEndian, - DicomTransferSyntax.ImplicitVRLittleEndian, - DicomTransferSyntax.JPEG2000Lossless, - DicomTransferSyntax.JPEG2000Lossy, - DicomTransferSyntax.JPEGProcess1, - DicomTransferSyntax.JPEGProcess2_4, - DicomTransferSyntax.RLELossless, - }; - - private static readonly HashSet SupportedTransferSyntaxesOver8Bit = new HashSet - { - DicomTransferSyntax.DeflatedExplicitVRLittleEndian, - DicomTransferSyntax.ExplicitVRBigEndian, - DicomTransferSyntax.ExplicitVRLittleEndian, - DicomTransferSyntax.ImplicitVRLittleEndian, - DicomTransferSyntax.RLELossless, - }; - - public static bool CanTranscodeDataset(this DicomDataset ds, DicomTransferSyntax toTransferSyntax) - { - EnsureArg.IsNotNull(ds, nameof(ds)); - - if (toTransferSyntax == null) - { - return true; - } - - var fromTs = ds.InternalTransferSyntax; - if (!ds.TryGetSingleValue(DicomTag.BitsAllocated, out ushort bpp)) - { - return false; - } - - if (!ds.TryGetString(DicomTag.PhotometricInterpretation, out string photometricInterpretation)) - { - return false; - } - - if ((fromTs == DicomTransferSyntax.JPEG2000Lossless || fromTs == DicomTransferSyntax.JPEG2000Lossy) && - (toTransferSyntax == DicomTransferSyntax.JPEG2000Lossless || toTransferSyntax == DicomTransferSyntax.JPEG2000Lossy) && - ((photometricInterpretation == PhotometricInterpretation.YbrIct.Value) || - (photometricInterpretation == PhotometricInterpretation.YbrRct.Value))) - { - return false; - } - - if ((fromTs == DicomTransferSyntax.DeflatedExplicitVRLittleEndian || - fromTs == DicomTransferSyntax.ExplicitVRBigEndian || - fromTs == DicomTransferSyntax.ExplicitVRLittleEndian || - fromTs == DicomTransferSyntax.ImplicitVRLittleEndian || - fromTs == DicomTransferSyntax.RLELossless) && - (toTransferSyntax == DicomTransferSyntax.JPEG2000Lossless || - toTransferSyntax == DicomTransferSyntax.JPEG2000Lossy || - toTransferSyntax == DicomTransferSyntax.JPEGProcess1 || - toTransferSyntax == DicomTransferSyntax.JPEGProcess2_4) && - ((photometricInterpretation == PhotometricInterpretation.Rgb.Value) || - (photometricInterpretation == PhotometricInterpretation.YbrFull422.Value) || - (photometricInterpretation == PhotometricInterpretation.YbrPartial422.Value) || - (photometricInterpretation == PhotometricInterpretation.YbrPartial420.Value))) - { - return false; - } - - if ((fromTs == DicomTransferSyntax.JPEG2000Lossless || fromTs == DicomTransferSyntax.JPEG2000Lossy) && - ((photometricInterpretation == PhotometricInterpretation.Rgb.Value) || - (photometricInterpretation == PhotometricInterpretation.YbrFull422.Value) || - (photometricInterpretation == PhotometricInterpretation.YbrPartial422.Value) || - (photometricInterpretation == PhotometricInterpretation.YbrPartial420.Value))) - { - return false; - } - - if ((fromTs == DicomTransferSyntax.JPEGProcess1 || fromTs == DicomTransferSyntax.JPEGProcess2_4) && - ((photometricInterpretation == PhotometricInterpretation.Rgb.Value) || - (photometricInterpretation == PhotometricInterpretation.YbrFull.Value) || - (photometricInterpretation == PhotometricInterpretation.YbrFull422.Value) || - (photometricInterpretation == PhotometricInterpretation.YbrPartial422.Value) || - (photometricInterpretation == PhotometricInterpretation.YbrPartial420.Value) || - (photometricInterpretation == PhotometricInterpretation.YbrIct.Value) || - (photometricInterpretation == PhotometricInterpretation.YbrRct.Value))) - { - return false; - } - - // Bug in fo-dicom 4.0.1 - if ((toTransferSyntax == DicomTransferSyntax.JPEGProcess1 || toTransferSyntax == DicomTransferSyntax.JPEGProcess2_4) && - ((photometricInterpretation == PhotometricInterpretation.Monochrome1.Value) || - (photometricInterpretation == PhotometricInterpretation.Monochrome2.Value) || - (photometricInterpretation == PhotometricInterpretation.YbrFull.Value) || - (photometricInterpretation == PhotometricInterpretation.YbrIct.Value) || - (photometricInterpretation == PhotometricInterpretation.YbrRct.Value))) - { - return false; - } - - if (((bpp > 8) && SupportedTransferSyntaxesOver8Bit.Contains(toTransferSyntax) && SupportedTransferSyntaxesOver8Bit.Contains(fromTs)) || - ((bpp <= 8) && SupportedTransferSyntaxes8Bit.Contains(toTransferSyntax) && SupportedTransferSyntaxes8Bit.Contains(fromTs))) - { - return true; - } - - return false; - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/DicomFileExtensions.cs b/src/Microsoft.Health.Dicom.Core/Features/Retrieve/DicomFileExtensions.cs deleted file mode 100644 index 83a6eb9a5f..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/DicomFileExtensions.cs +++ /dev/null @@ -1,95 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using EnsureThat; -using FellowOakDicom; -using FellowOakDicom.Imaging; -using FellowOakDicom.IO.Writer; -using FellowOakDicom.IO; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.IO; - -namespace Microsoft.Health.Dicom.Core.Features.Retrieve; - -public static class DicomFileExtensions -{ - public static DicomPixelData GetPixelDataAndValidateFrames(this DicomFile dicomFile, IEnumerable frames) - { - var pixelData = GetPixelData(dicomFile); - ValidateFrames(pixelData, frames); - - return pixelData; - } - - public static bool TryGetPixelData(this DicomDataset dataset, out DicomPixelData dicomPixelData) - { - EnsureArg.IsNotNull(dataset, nameof(dataset)); - dicomPixelData = null; - - // Validate the dataset has the correct DICOM tags. - if (!dataset.Contains(DicomTag.BitsAllocated) || - !dataset.Contains(DicomTag.Columns) || - !dataset.Contains(DicomTag.Rows) || - !dataset.Contains(DicomTag.PixelData)) - { - return false; - } - dicomPixelData = DicomPixelData.Create(dataset); - return true; - } - - public static DicomPixelData GetPixelData(this DicomFile dicomFile) - { - EnsureArg.IsNotNull(dicomFile, nameof(dicomFile)); - DicomDataset dataset = dicomFile.Dataset; - - // Validate the dataset has the correct DICOM tags. - if (!TryGetPixelData(dataset, out DicomPixelData dicomPixelData)) - { - throw new FrameNotFoundException(); - } - - return dicomPixelData; - } - - public static void ValidateFrames(DicomPixelData pixelData, IEnumerable frames) - { - // Note: We look for any frame value that is less than zero, or greater than number of frames. - var missingFrames = frames.Where(x => x >= pixelData.NumberOfFrames || x < 0).ToArray(); - - // If any missing frames, throw not found exception for the specific frames not found. - if (missingFrames.Length > 0) - { - throw new FrameNotFoundException(); - } - } - - /// - /// Given a dicom file, the method will return the length of the dataset. - /// - /// Dicom file - /// RecyclableMemoryStreamManager to get Memory stream - /// Dataset size - public static async Task GetByteLengthAsync(this DicomFile dcmFile, RecyclableMemoryStreamManager recyclableMemoryStreamManager) - { - EnsureArg.IsNotNull(dcmFile, nameof(dcmFile)); - EnsureArg.IsNotNull(recyclableMemoryStreamManager, nameof(recyclableMemoryStreamManager)); - - DicomDataset dataset = dcmFile.Dataset; - - var writeOptions = new DicomWriteOptions(); - using MemoryStream resultStream = recyclableMemoryStreamManager.GetStream(tag: nameof(GetByteLengthAsync)); - - var target = new StreamByteTarget(resultStream); - var writer = new DicomFileWriter(writeOptions); - await writer.WriteAsync(target, dcmFile.FileMetaInfo, dataset); - - return resultStream.Length; - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/ETagGenerator.cs b/src/Microsoft.Health.Dicom.Core/Features/Retrieve/ETagGenerator.cs deleted file mode 100644 index 49bf3e8bba..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/ETagGenerator.cs +++ /dev/null @@ -1,45 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using EnsureThat; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Messages; - -namespace Microsoft.Health.Dicom.Core.Features.Retrieve; - -public class ETagGenerator : IETagGenerator -{ - public string GetETag(ResourceType resourceType, IReadOnlyList retrieveInstances) - { - EnsureArg.IsTrue( - resourceType == ResourceType.Study || - resourceType == ResourceType.Series || - resourceType == ResourceType.Instance, - nameof(resourceType)); - EnsureArg.HasItems(retrieveInstances); - - string eTag = string.Empty; - long maxWatermark = retrieveInstances.Max(ri => ri.VersionedInstanceIdentifier.Version); - - switch (resourceType) - { - case ResourceType.Study: - case ResourceType.Series: - int countInstances = retrieveInstances.Count; - eTag = $"{maxWatermark}-{countInstances}"; - break; - case ResourceType.Instance: - eTag = maxWatermark.ToString(CultureInfo.InvariantCulture); - break; - default: - break; - } - - return eTag; - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/FrameHandler.cs b/src/Microsoft.Health.Dicom.Core/Features/Retrieve/FrameHandler.cs deleted file mode 100644 index fcbac514db..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/FrameHandler.cs +++ /dev/null @@ -1,69 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using EnsureThat; -using FellowOakDicom; -using FellowOakDicom.Imaging; -using FellowOakDicom.IO.Buffer; -using Microsoft.IO; - -namespace Microsoft.Health.Dicom.Core.Features.Retrieve; - -public class FrameHandler : IFrameHandler -{ - private readonly ITranscoder _transcoder; - private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager; - - public FrameHandler( - ITranscoder transcoder, - RecyclableMemoryStreamManager recyclableMemoryStreamManager) - { - EnsureArg.IsNotNull(transcoder, nameof(transcoder)); - EnsureArg.IsNotNull(recyclableMemoryStreamManager, nameof(recyclableMemoryStreamManager)); - - _transcoder = transcoder; - _recyclableMemoryStreamManager = recyclableMemoryStreamManager; - } - - public async Task> GetFramesResourceAsync(Stream stream, IEnumerable frames, bool originalTransferSyntaxRequested, string requestedRepresentation) - { - EnsureArg.IsNotNull(stream, nameof(stream)); - EnsureArg.IsNotNull(frames, nameof(frames)); - - DicomFile dicomFile = await DicomFile.OpenAsync(stream); - - // Validate requested frame index exists in file and retrieve the pixel data associated with the file. - DicomPixelData pixelData = dicomFile.GetPixelDataAndValidateFrames(frames); - - if (!originalTransferSyntaxRequested && !dicomFile.Dataset.InternalTransferSyntax.Equals(DicomTransferSyntax.Parse(requestedRepresentation))) - { - return frames.Select(frame => new LazyTransformReadOnlyStream( - dicomFile, - df => _transcoder.TranscodeFrame(df, frame, requestedRepresentation))) - .ToArray(); - } - else - { - return frames.Select( - frame => new LazyTransformReadOnlyStream( - dicomFile, - df => GetFrameAsDicomData(pixelData, frame))) - .ToArray(); - } - } - - private RecyclableMemoryStream GetFrameAsDicomData(DicomPixelData pixelData, int frame) - { - EnsureArg.IsNotNull(pixelData, nameof(pixelData)); - - IByteBuffer resultByteBuffer = pixelData.GetFrame(frame); - - return _recyclableMemoryStreamManager.GetStream(nameof(GetFrameAsDicomData), resultByteBuffer.Data, 0, resultByteBuffer.Data.Length); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/FramesRangeCache.cs b/src/Microsoft.Health.Dicom.Core/Features/Retrieve/FramesRangeCache.cs deleted file mode 100644 index e1f7b2875d..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/FramesRangeCache.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Model; - -namespace Microsoft.Health.Dicom.Core.Features.Retrieve; - -public class FramesRangeCache : EphemeralMemoryCache>, IFramesRangeCache -{ - public FramesRangeCache(IOptions configuration, ILoggerFactory loggerFactory, ILogger logger) - : base(configuration, loggerFactory, logger) - { - } -} - diff --git a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/IAcceptHeaderHandler.cs b/src/Microsoft.Health.Dicom.Core/Features/Retrieve/IAcceptHeaderHandler.cs deleted file mode 100644 index 652adbddc8..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/IAcceptHeaderHandler.cs +++ /dev/null @@ -1,15 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using Microsoft.Health.Dicom.Core.Messages; -using Microsoft.Health.Dicom.Core.Messages.Retrieve; - -namespace Microsoft.Health.Dicom.Core.Features.Retrieve; - -public interface IAcceptHeaderHandler -{ - AcceptHeader GetValidAcceptHeader(ResourceType resourceType, IReadOnlyCollection acceptHeaders); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/IETagGenerator.cs b/src/Microsoft.Health.Dicom.Core/Features/Retrieve/IETagGenerator.cs deleted file mode 100644 index 5acf9112a0..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/IETagGenerator.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Messages; - -namespace Microsoft.Health.Dicom.Core.Features.Retrieve; - -public interface IETagGenerator -{ - /// - /// - /// Get ETag from the list of instances to retrieve. - /// - /// - /// For study and series resource types, Etag is calculated using the following formula: - /// $"{Max(Instance Watermark)}-{Count(Instance)}" - /// - /// - /// For instance, its watermark is returned as the ETag. - /// - /// - /// Resource Type. - /// Retrieve Instances. - /// ETag. - string GetETag(ResourceType resourceType, IReadOnlyList retrieveInstances); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/IFrameHandler.cs b/src/Microsoft.Health.Dicom.Core/Features/Retrieve/IFrameHandler.cs deleted file mode 100644 index 9b4c8a2b55..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/IFrameHandler.cs +++ /dev/null @@ -1,15 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; - -namespace Microsoft.Health.Dicom.Core.Features.Retrieve; - -public interface IFrameHandler -{ - Task> GetFramesResourceAsync(Stream stream, IEnumerable frames, bool originalTransferSyntaxRequested, string requestedRepresentation); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/IFramesRangeCache.cs b/src/Microsoft.Health.Dicom.Core/Features/Retrieve/IFramesRangeCache.cs deleted file mode 100644 index ba77f61706..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/IFramesRangeCache.cs +++ /dev/null @@ -1,21 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Health.Dicom.Core.Features.Model; - -namespace Microsoft.Health.Dicom.Core.Features.Retrieve; - -public interface IFramesRangeCache -{ - public Task> GetAsync( - object key, - long input, - Func>> asyncFactory, - CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/IInstanceMetadataCache.cs b/src/Microsoft.Health.Dicom.Core/Features/Retrieve/IInstanceMetadataCache.cs deleted file mode 100644 index e9b135584a..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/IInstanceMetadataCache.cs +++ /dev/null @@ -1,20 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Health.Dicom.Core.Features.Model; - -namespace Microsoft.Health.Dicom.Core.Features.Retrieve; - -public interface IInstanceMetadataCache -{ - public Task GetAsync( - object key, - InstanceIdentifier input, - Func> asyncFactory, - CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/IInstanceStore.cs b/src/Microsoft.Health.Dicom.Core/Features/Retrieve/IInstanceStore.cs deleted file mode 100644 index 5f1f3a3b1e..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/IInstanceStore.cs +++ /dev/null @@ -1,177 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Core.Models; - -namespace Microsoft.Health.Dicom.Core.Features.Retrieve; - -public interface IInstanceStore -{ - /// - /// Gets identifiers of instances in a study. - /// - /// The partition. - /// The study identifier. - /// An optional cancellation token. - /// Instance identifiers. - Task> GetInstanceIdentifiersInStudyAsync( - Partition partition, - string studyInstanceUid, - CancellationToken cancellationToken = default); - - /// - /// Gets identifiers of instances in a series. - /// - /// The partition. - /// The study identifier. - /// The series identifier. - /// An optional cancellation token. - /// Instance identifiers. - Task> GetInstanceIdentifiersInSeriesAsync( - Partition partition, - string studyInstanceUid, - string seriesInstanceUid, - CancellationToken cancellationToken = default); - - /// - /// Gets identifiers of instances in an instance. - /// - /// The partition. - /// The study identifier. - /// The series identifier. - /// The instance identifier. - /// An optional cancellation token. - /// Instance identifiers. - Task> GetInstanceIdentifierAsync( - Partition partition, - string studyInstanceUid, - string seriesInstanceUid, - string sopInstanceUid, - CancellationToken cancellationToken = default); - - /// - /// Gets identifiers of instances within the given range of watermarks. - /// - /// The watermark range - /// The index status - /// The cancellation token - /// The instanceidentifiers - Task> GetInstanceIdentifiersByWatermarkRangeAsync( - WatermarkRange watermarkRange, - IndexStatus indexStatus, - CancellationToken cancellationToken = default); - - /// - /// Gets identifiers of instances within the given range of watermarks which need content length backfilled. - /// - /// The watermark range - /// The cancellation token - /// The instanceidentifiers - Task> GetContentLengthBackFillInstanceIdentifiersByWatermarkRangeAsync( - WatermarkRange watermarkRange, - CancellationToken cancellationToken = default); - - /// - /// Asynchronously retrieves the specified number of instance batches. - /// - /// The desired size of each batch. - /// The maximum number of batches. - /// The index status - /// An optional maximum watermark to consider. - /// - /// The token to monitor for cancellation requests. The default value is . - /// - /// - /// A task representing the asynchronous get operation. The value of its - /// property contains a list of batches as defined by their smallest and largest watermark. - /// The size of the collection is at most the value of the parameter. - /// - /// - /// or is less than 1. - /// - /// The was canceled. - Task> GetInstanceBatchesAsync( - int batchSize, - int batchCount, - IndexStatus indexStatus, - long? maxWatermark = null, - CancellationToken cancellationToken = default); - - /// - /// Gets identifiers of instances with additional properties. - /// - /// The partition. - /// The study identifier. - /// The series identifier. - /// The instance identifier. - /// True, if requesting original version. If original version is null it will uses the version. - /// An optional cancellation token. - /// Instance identifiers. - Task> GetInstanceIdentifierWithPropertiesAsync( - Partition partition, - string studyInstanceUid, - string seriesInstanceUid = null, - string sopInstanceUid = null, - bool isInitialVersion = false, - CancellationToken cancellationToken = default); - - /// - /// Asynchronously retrieves the specified number of instance batches filtered by timestamp. - /// - /// The desired size of each batch. - /// The maximum number of batches. - /// The index status - /// Start filterstamp - /// End filterstamp - /// An optional maximum watermark to consider. - /// - /// The token to monitor for cancellation requests. The default value is . - /// - /// - /// A task representing the asynchronous get operation. The value of its - /// property contains a list of batches as defined by their smallest and largest watermark. - /// The size of the collection is at most the value of the parameter. - /// - /// - /// or is less than 1. - /// - /// The was canceled. - Task> GetInstanceBatchesByTimeStampAsync( - int batchSize, - int batchCount, - IndexStatus indexStatus, - DateTimeOffset startTimeStamp, - DateTimeOffset endTimeStamp, - long? maxWatermark = null, - CancellationToken cancellationToken = default); - - /// - /// Asynchronously retrieves the specified number of instance batches whose content length needs to be backfilled. - /// - /// The desired size of each batch. - /// The maximum number of batches. - /// - /// The token to monitor for cancellation requests. The default value is . - /// - /// - /// A task representing the asynchronous get operation. The value of its - /// property contains a list of batches as defined by their smallest and largest watermark. - /// The size of the collection is at most the value of the parameter. - /// - /// - /// or is less than 1. - /// - /// The was canceled. - Task> GetContentLengthBackFillInstanceBatches( - int batchSize, - int batchCount, - CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/IRetrieveMetadataService.cs b/src/Microsoft.Health.Dicom.Core/Features/Retrieve/IRetrieveMetadataService.cs deleted file mode 100644 index 819a0a41ad..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/IRetrieveMetadataService.cs +++ /dev/null @@ -1,34 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Health.Dicom.Core.Messages.Retrieve; - -namespace Microsoft.Health.Dicom.Core.Features.Retrieve; - -public interface IRetrieveMetadataService -{ - Task RetrieveStudyInstanceMetadataAsync( - string studyInstanceUid, - string ifNoneMatch = null, - bool isOriginalVersionRequested = false, - CancellationToken cancellationToken = default); - - Task RetrieveSeriesInstanceMetadataAsync( - string studyInstanceUid, - string seriesInstanceUid, - string ifNoneMatch = null, - bool isOriginalVersionRequested = false, - CancellationToken cancellationToken = default); - - Task RetrieveSopInstanceMetadataAsync( - string studyInstanceUid, - string seriesInstanceUid, - string sopInstanceUid, - string ifNoneMatch = null, - bool isOriginalVersionRequested = false, - CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/IRetrieveRenderedService.cs b/src/Microsoft.Health.Dicom.Core/Features/Retrieve/IRetrieveRenderedService.cs deleted file mode 100644 index 4567d9b18f..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/IRetrieveRenderedService.cs +++ /dev/null @@ -1,16 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Health.Dicom.Core.Messages.Retrieve; - -namespace Microsoft.Health.Dicom.Core.Features.Retrieve; -public interface IRetrieveRenderedService -{ - Task RetrieveRenderedImageAsync( - RetrieveRenderedRequest request, - CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/IRetrieveResourceService.cs b/src/Microsoft.Health.Dicom.Core/Features/Retrieve/IRetrieveResourceService.cs deleted file mode 100644 index 72519bf019..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/IRetrieveResourceService.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Health.Dicom.Core.Messages.Retrieve; - -namespace Microsoft.Health.Dicom.Core.Features.Retrieve; - -public interface IRetrieveResourceService -{ - Task GetInstanceResourceAsync( - RetrieveResourceRequest message, - CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/ITranscoder.cs b/src/Microsoft.Health.Dicom.Core/Features/Retrieve/ITranscoder.cs deleted file mode 100644 index 78ecebc7dd..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/ITranscoder.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.IO; -using System.Threading.Tasks; -using FellowOakDicom; - -namespace Microsoft.Health.Dicom.Core.Features.Retrieve; - -public interface ITranscoder -{ - public Task TranscodeFileAsync(Stream stream, string requestedTransferSyntax); - - Stream TranscodeFrame(DicomFile dicomFile, int frameIndex, string requestedTransferSyntax); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/InstanceMetadataCache.cs b/src/Microsoft.Health.Dicom.Core/Features/Retrieve/InstanceMetadataCache.cs deleted file mode 100644 index 2d4712b627..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/InstanceMetadataCache.cs +++ /dev/null @@ -1,20 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Model; - -namespace Microsoft.Health.Dicom.Core.Features.Retrieve; - -public class InstanceMetadataCache : EphemeralMemoryCache, IInstanceMetadataCache -{ - public InstanceMetadataCache(IOptions configuration, ILoggerFactory loggerFactory, ILogger logger) - : base(configuration, loggerFactory, logger) - { - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/InstanceStoreExtensions.cs b/src/Microsoft.Health.Dicom.Core/Features/Retrieve/InstanceStoreExtensions.cs deleted file mode 100644 index e8d4d98156..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/InstanceStoreExtensions.cs +++ /dev/null @@ -1,55 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Core.Messages; - -namespace Microsoft.Health.Dicom.Core.Features.Retrieve; - -public static class InstanceStoreExtensions -{ - public static async Task> GetInstancesWithProperties( - this IInstanceStore instanceStore, - ResourceType resourceType, - Partition partition, - string studyInstanceUid, - string seriesInstanceUid, - string sopInstanceUid, - bool isInitialVersion = false, - CancellationToken cancellationToken = default) - { - EnsureArg.IsNotNull(instanceStore, nameof(instanceStore)); - - IReadOnlyList instancesToRetrieve = await instanceStore.GetInstanceIdentifierWithPropertiesAsync(partition, studyInstanceUid, seriesInstanceUid, sopInstanceUid, isInitialVersion, cancellationToken); - - if (!instancesToRetrieve.Any()) - { - ThrowNotFoundException(resourceType); - } - - return instancesToRetrieve; - } - - private static void ThrowNotFoundException(ResourceType resourceType) - { - switch (resourceType) - { - case ResourceType.Frames: - case ResourceType.Instance: - throw new InstanceNotFoundException(); - case ResourceType.Series: - throw new InstanceNotFoundException(DicomCoreResource.SeriesInstanceNotFound); - case ResourceType.Study: - throw new InstanceNotFoundException(DicomCoreResource.StudyInstanceNotFound); - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/LazyTransformReadOnlyStream.cs b/src/Microsoft.Health.Dicom.Core/Features/Retrieve/LazyTransformReadOnlyStream.cs deleted file mode 100644 index 16f9299909..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/LazyTransformReadOnlyStream.cs +++ /dev/null @@ -1,81 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.IO; -using EnsureThat; - -namespace Microsoft.Health.Dicom.Core.Features.Retrieve; - -internal class LazyTransformReadOnlyStream : Stream -{ - private readonly object _lockObject = new object(); - private readonly Func _transformFunction; - private readonly T _transformInput; - private Stream _outputStream; - private bool _disposed; - - public LazyTransformReadOnlyStream(T transformInput, Func transformFunction) - { - EnsureArg.IsNotNull(transformFunction, nameof(transformFunction)); - - _transformInput = transformInput; - _transformFunction = transformFunction; - } - - public override bool CanRead => true; - - public override bool CanSeek => true; - - public override bool CanWrite => false; - - public override long Length => GetOutputStream().Length; - - public override long Position { get => GetOutputStream().Position; set => GetOutputStream().Position = value; } - - public override void Flush() => GetOutputStream().Flush(); - - public override int Read(byte[] buffer, int offset, int count) => GetOutputStream().Read(buffer, offset, count); - - public override long Seek(long offset, SeekOrigin origin) => GetOutputStream().Seek(offset, origin); - - public override void SetLength(long value) => throw new NotSupportedException(); - - public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); - - protected override void Dispose(bool disposing) - { - if (_disposed) - { - return; - } - - base.Dispose(disposing); - - if (disposing) - { - _outputStream?.Dispose(); - _outputStream = null; - } - - _disposed = true; - } - - private Stream GetOutputStream() - { - if (_outputStream == null) - { - lock (_lockObject) - { - if (_outputStream == null) - { - _outputStream = _transformFunction(_transformInput); - } - } - } - - return _outputStream; - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/PayloadTypes.cs b/src/Microsoft.Health.Dicom.Core/Features/Retrieve/PayloadTypes.cs deleted file mode 100644 index e1df91fa0a..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/PayloadTypes.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; - -namespace Microsoft.Health.Dicom.Core.Features.Retrieve; - -[Flags] -public enum PayloadTypes -{ - None = 0, - SinglePart = 1, - MultipartRelated = 2, - SinglePartOrMultipartRelated = SinglePart | MultipartRelated, -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/RetrieveHelpers.cs b/src/Microsoft.Health.Dicom.Core/Features/Retrieve/RetrieveHelpers.cs deleted file mode 100644 index 7d3deab22d..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/RetrieveHelpers.cs +++ /dev/null @@ -1,47 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - - -using System.Globalization; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.Extensions.Logging; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Partitioning; - -namespace Microsoft.Health.Dicom.Core.Features.Retrieve; -internal static class RetrieveHelpers -{ - public static async Task CheckFileSize( - IFileStore blobDataStore, - long maxDicomFileSize, - long version, - Partition partition, - FileProperties fileProperties, - bool render, - ILogger logger, - CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(blobDataStore, nameof(blobDataStore)); - - FileProperties filePropertiesWithContentLength = await blobDataStore.GetFilePropertiesAsync(version, partition, fileProperties, cancellationToken); - - // limit the file size that can be read in memory - if (filePropertiesWithContentLength.ContentLength > maxDicomFileSize) - { - logger.LogInformation("Requested DICOM instance size is above the supported limit. Actual size {ActualLength} bytes. IsRender {IsRender}", filePropertiesWithContentLength.ContentLength, render); - - if (render) - { - throw new NotAcceptableException(string.Format(CultureInfo.CurrentCulture, DicomCoreResource.RenderFileTooLarge, maxDicomFileSize)); - } - throw new NotAcceptableException(string.Format(CultureInfo.CurrentCulture, DicomCoreResource.RetrieveServiceFileTooBig, maxDicomFileSize)); - } - - return filePropertiesWithContentLength; - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/RetrieveMetadataHandler.cs b/src/Microsoft.Health.Dicom.Core/Features/Retrieve/RetrieveMetadataHandler.cs deleted file mode 100644 index a40bff2143..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/RetrieveMetadataHandler.cs +++ /dev/null @@ -1,66 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Diagnostics; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using MediatR; -using Microsoft.Health.Core.Features.Security.Authorization; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Security; -using Microsoft.Health.Dicom.Core.Messages; -using Microsoft.Health.Dicom.Core.Messages.Retrieve; - -namespace Microsoft.Health.Dicom.Core.Features.Retrieve; - -internal class RetrieveMetadataHandler : BaseHandler, IRequestHandler -{ - private readonly IRetrieveMetadataService _retrieveMetadataService; - - public RetrieveMetadataHandler(IAuthorizationService authorizationService, IRetrieveMetadataService retrieveMetadataService) - : base(authorizationService) - { - _retrieveMetadataService = EnsureArg.IsNotNull(retrieveMetadataService, nameof(retrieveMetadataService)); - } - - public async Task Handle(RetrieveMetadataRequest request, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(request, nameof(request)); - - if (await AuthorizationService.CheckAccess(DataActions.Read, cancellationToken) != DataActions.Read) - { - throw new UnauthorizedDicomActionException(DataActions.Read); - } - - ValidateRetrieveMetadataRequest(request); - - RetrieveMetadataResponse metadataResponse = null; - - switch (request.ResourceType) - { - case ResourceType.Study: - metadataResponse = await _retrieveMetadataService.RetrieveStudyInstanceMetadataAsync(request.StudyInstanceUid, request.IfNoneMatch, request.IsOriginalVersionRequested, cancellationToken); - break; - case ResourceType.Series: - metadataResponse = await _retrieveMetadataService.RetrieveSeriesInstanceMetadataAsync(request.StudyInstanceUid, request.SeriesInstanceUid, request.IfNoneMatch, request.IsOriginalVersionRequested, cancellationToken); - break; - case ResourceType.Instance: - metadataResponse = await _retrieveMetadataService.RetrieveSopInstanceMetadataAsync(request.StudyInstanceUid, request.SeriesInstanceUid, request.SopInstanceUid, request.IfNoneMatch, request.IsOriginalVersionRequested, cancellationToken); - break; - default: - Debug.Fail($"Unknown retrieve metadata transaction type: {request.ResourceType}", nameof(request)); - break; - } - - return metadataResponse; - } - - private static void ValidateRetrieveMetadataRequest(RetrieveMetadataRequest request) - { - RetrieveRequestValidator.ValidateInstanceIdentifiers(request.ResourceType, request.StudyInstanceUid, request.SeriesInstanceUid, request.SopInstanceUid); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/RetrieveMetadataService.cs b/src/Microsoft.Health.Dicom.Core/Features/Retrieve/RetrieveMetadataService.cs deleted file mode 100644 index ae1f68266e..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/RetrieveMetadataService.cs +++ /dev/null @@ -1,131 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using FellowOakDicom; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Context; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Core.Features.Telemetry; -using Microsoft.Health.Dicom.Core.Messages; -using Microsoft.Health.Dicom.Core.Messages.Retrieve; - -namespace Microsoft.Health.Dicom.Core.Features.Retrieve; - -public class RetrieveMetadataService : IRetrieveMetadataService -{ - private readonly IInstanceStore _instanceStore; - private readonly IMetadataStore _metadataStore; - private readonly IETagGenerator _eTagGenerator; - private readonly IDicomRequestContextAccessor _contextAccessor; - private readonly RetrieveConfiguration _options; - private readonly RetrieveMeter _retrieveMeter; - - public RetrieveMetadataService( - IInstanceStore instanceStore, - IMetadataStore metadataStore, - IETagGenerator eTagGenerator, - IDicomRequestContextAccessor contextAccessor, - RetrieveMeter retrieveMeter, - IOptions options) - { - _instanceStore = EnsureArg.IsNotNull(instanceStore, nameof(instanceStore)); - _metadataStore = EnsureArg.IsNotNull(metadataStore, nameof(metadataStore)); - _eTagGenerator = EnsureArg.IsNotNull(eTagGenerator, nameof(eTagGenerator)); - _contextAccessor = EnsureArg.IsNotNull(contextAccessor, nameof(contextAccessor)); - _retrieveMeter = EnsureArg.IsNotNull(retrieveMeter, nameof(retrieveMeter)); - _options = EnsureArg.IsNotNull(options?.Value, nameof(options)); - } - - public async Task RetrieveStudyInstanceMetadataAsync(string studyInstanceUid, string ifNoneMatch = null, bool isOriginalVersionRequested = false, CancellationToken cancellationToken = default) - { - IReadOnlyList retrieveInstances = await _instanceStore.GetInstancesWithProperties( - ResourceType.Study, - GetPartition(), - studyInstanceUid, - seriesInstanceUid: null, - sopInstanceUid: null, - isOriginalVersionRequested, - cancellationToken); - - string eTag = _eTagGenerator.GetETag(ResourceType.Study, retrieveInstances); - bool isCacheValid = IsCacheValid(eTag, ifNoneMatch); - return RetrieveMetadata(retrieveInstances, isCacheValid, eTag, isOriginalVersionRequested, cancellationToken); - } - - public async Task RetrieveSeriesInstanceMetadataAsync(string studyInstanceUid, string seriesInstanceUid, string ifNoneMatch = null, bool isOriginalVersionRequested = false, CancellationToken cancellationToken = default) - { - IReadOnlyList retrieveInstances = await _instanceStore.GetInstancesWithProperties( - ResourceType.Series, - GetPartition(), - studyInstanceUid, - seriesInstanceUid, - sopInstanceUid: null, - isOriginalVersionRequested, - cancellationToken); - - string eTag = _eTagGenerator.GetETag(ResourceType.Series, retrieveInstances); - bool isCacheValid = IsCacheValid(eTag, ifNoneMatch); - return RetrieveMetadata(retrieveInstances, isCacheValid, eTag, isOriginalVersionRequested, cancellationToken); - } - - public async Task RetrieveSopInstanceMetadataAsync(string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid, string ifNoneMatch = null, bool isOriginalVersionRequested = false, CancellationToken cancellationToken = default) - { - IReadOnlyList retrieveInstances = await _instanceStore.GetInstancesWithProperties( - ResourceType.Instance, - GetPartition(), - studyInstanceUid, - seriesInstanceUid, - sopInstanceUid, - isOriginalVersionRequested, - cancellationToken); - - string eTag = _eTagGenerator.GetETag(ResourceType.Instance, retrieveInstances); - bool isCacheValid = IsCacheValid(eTag, ifNoneMatch); - return RetrieveMetadata(retrieveInstances, isCacheValid, eTag, isOriginalVersionRequested, cancellationToken); - } - - private RetrieveMetadataResponse RetrieveMetadata(IReadOnlyList instancesToRetrieve, bool isCacheValid, string eTag, bool isOriginalVersionRequested, CancellationToken cancellationToken) - { - _contextAccessor.RequestContext.PartCount = instancesToRetrieve.Count; - _retrieveMeter.RetrieveInstanceMetadataCount.Add(instancesToRetrieve.Count); - - // Retrieve metadata instances only if cache is not valid. - IAsyncEnumerable instanceMetadata = isCacheValid - ? AsyncEnumerable.Empty() - : instancesToRetrieve.SelectParallel( - (x, t) => new ValueTask( - _metadataStore.GetInstanceMetadataAsync( - x.GetVersion(isOriginalVersionRequested), t)), - new ParallelEnumerationOptions { MaxDegreeOfParallelism = _options.MaxDegreeOfParallelism }, - cancellationToken); - - return new RetrieveMetadataResponse(instanceMetadata, isCacheValid, eTag); - } - - /// - /// Check if cache is valid. - /// Cache is regarded as valid if the following criteria passes: - /// 1. User has passed If-None-Match in the header. - /// 2. Calculated ETag is equals to the If-None-Match header field. - /// - /// ETag. - /// If-None-Match - /// True if cache is valid, i.e. content has not modified, else returns false. - private static bool IsCacheValid(string eTag, string ifNoneMatch) - => !string.IsNullOrEmpty(ifNoneMatch) && string.Equals(ifNoneMatch, eTag, StringComparison.OrdinalIgnoreCase); - - private Partition GetPartition() - => _contextAccessor.RequestContext.GetPartition(); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/RetrieveRenderedHandler.cs b/src/Microsoft.Health.Dicom.Core/Features/Retrieve/RetrieveRenderedHandler.cs deleted file mode 100644 index ed186d32c8..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/RetrieveRenderedHandler.cs +++ /dev/null @@ -1,51 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using MediatR; -using Microsoft.Health.Core.Features.Security.Authorization; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Security; -using Microsoft.Health.Dicom.Core.Messages; -using Microsoft.Health.Dicom.Core.Messages.Retrieve; - -namespace Microsoft.Health.Dicom.Core.Features.Retrieve; -internal class RetrieveRenderedHandler : BaseHandler, IRequestHandler -{ - private readonly IRetrieveRenderedService _retrieveRenderedService; - - public RetrieveRenderedHandler(IAuthorizationService authorizationService, IRetrieveRenderedService retrieveRenderedService) - : base(authorizationService) - { - _retrieveRenderedService = EnsureArg.IsNotNull(retrieveRenderedService, nameof(retrieveRenderedService)); - } - - public async Task Handle(RetrieveRenderedRequest request, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(request, nameof(request)); - - if (await AuthorizationService.CheckAccess(DataActions.Read, cancellationToken) != DataActions.Read) - { - throw new UnauthorizedDicomActionException(DataActions.Read); - } - - ValidateRetrieveRenderedRequest(request); - - return await _retrieveRenderedService.RetrieveRenderedImageAsync(request, cancellationToken); - } - - private static void ValidateRetrieveRenderedRequest(RetrieveRenderedRequest request) - { - RetrieveRequestValidator.ValidateInstanceIdentifiers(request.ResourceType, request.StudyInstanceUid, request.SeriesInstanceUid, request.SopInstanceUid); - - if (request.ResourceType == ResourceType.Frames) - { - RetrieveRequestValidator.ValidateFrames(new[] { request.FrameNumber }); - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/RetrieveRenderedService.cs b/src/Microsoft.Health.Dicom.Core/Features/Retrieve/RetrieveRenderedService.cs deleted file mode 100644 index 9882c2e39c..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/RetrieveRenderedService.cs +++ /dev/null @@ -1,181 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using FellowOakDicom; -using FellowOakDicom.Imaging; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Extensions.Primitives; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Context; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Features.Telemetry; -using Microsoft.Health.Dicom.Core.Messages; -using Microsoft.Health.Dicom.Core.Messages.Retrieve; -using Microsoft.Health.Dicom.Core.Web; -using Microsoft.IO; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Formats.Jpeg; - -namespace Microsoft.Health.Dicom.Core.Features.Retrieve; -public class RetrieveRenderedService : IRetrieveRenderedService -{ - private readonly IFileStore _blobDataStore; - private readonly IInstanceStore _instanceStore; - private readonly IDicomRequestContextAccessor _dicomRequestContextAccessor; - private readonly RetrieveConfiguration _retrieveConfiguration; - private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager; - private readonly ILogger _logger; - private readonly RetrieveMeter _retrieveMeter; - - public RetrieveRenderedService( - IInstanceStore instanceStore, - IFileStore blobDataStore, - IDicomRequestContextAccessor dicomRequestContextAccessor, - IOptionsSnapshot retrieveConfiguration, - RecyclableMemoryStreamManager recyclableMemoryStreamManager, - RetrieveMeter retrieveMeter, - ILogger logger) - { - EnsureArg.IsNotNull(instanceStore, nameof(instanceStore)); - EnsureArg.IsNotNull(blobDataStore, nameof(blobDataStore)); - EnsureArg.IsNotNull(dicomRequestContextAccessor, nameof(dicomRequestContextAccessor)); - EnsureArg.IsNotNull(retrieveConfiguration?.Value, nameof(retrieveConfiguration)); - EnsureArg.IsNotNull(recyclableMemoryStreamManager, nameof(recyclableMemoryStreamManager)); - _retrieveMeter = EnsureArg.IsNotNull(retrieveMeter, nameof(retrieveMeter)); - EnsureArg.IsNotNull(logger, nameof(logger)); - - _instanceStore = instanceStore; - _blobDataStore = blobDataStore; - _dicomRequestContextAccessor = dicomRequestContextAccessor; - _retrieveConfiguration = retrieveConfiguration?.Value; - _recyclableMemoryStreamManager = recyclableMemoryStreamManager; - _logger = logger; - } - - public async Task RetrieveRenderedImageAsync(RetrieveRenderedRequest request, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(request, nameof(request)); - - if (request.Quality < 1 || request.Quality > 100) - { - throw new BadRequestException(DicomCoreResource.InvalidImageQuality); - } - - // To keep track of how long render operation is taking - Stopwatch sw = new Stopwatch(); - - var partition = _dicomRequestContextAccessor.RequestContext.GetPartition(); - _dicomRequestContextAccessor.RequestContext.PartCount = 1; - AcceptHeader returnHeader = GetValidRenderAcceptHeader(request.AcceptHeaders); - - try - { - // this call throws NotFound when zero instance found - InstanceMetadata instance = (await _instanceStore.GetInstancesWithProperties( - ResourceType.Instance, partition, request.StudyInstanceUid, request.SeriesInstanceUid, request.SopInstanceUid, isInitialVersion: false, cancellationToken))[0]; - - FileProperties fileProperties = await RetrieveHelpers.CheckFileSize(_blobDataStore, _retrieveConfiguration.MaxDicomFileSize, instance.VersionedInstanceIdentifier.Version, partition, instance.InstanceProperties.FileProperties, true, _logger, cancellationToken); - _logger.LogInformation( - "Retrieving rendered Instance for watermark {Watermark} of size {ContentLength}", instance.VersionedInstanceIdentifier.Version, fileProperties.ContentLength); - _retrieveMeter.RetrieveInstanceCount.Add( - fileProperties.ContentLength, - RetrieveMeter.RetrieveInstanceCountTelemetryDimension(isRendered: true)); - - using Stream stream = await _blobDataStore.GetFileAsync(instance.VersionedInstanceIdentifier.Version, instance.VersionedInstanceIdentifier.Partition, instance.InstanceProperties.FileProperties, cancellationToken); - sw.Start(); - - DicomFile dicomFile = await DicomFile.OpenAsync(stream, FileReadOption.ReadLargeOnDemand); - DicomPixelData dicomPixelData = dicomFile.GetPixelDataAndValidateFrames(new[] { request.FrameNumber }); - - Stream resultStream = await ConvertToImage(dicomFile, request.FrameNumber, returnHeader.MediaType.ToString(), request.Quality, cancellationToken); - string outputContentType = returnHeader.MediaType.ToString(); - - sw.Stop(); - _logger.LogInformation("Render from dicom to {OutputContentType}, uncompressed file size was {UncompressedFrameSize}, output frame size is {OutputFrameSize} and took {ElapsedMilliseconds} ms", outputContentType, stream.Length, resultStream.Length, sw.ElapsedMilliseconds); - - _dicomRequestContextAccessor.RequestContext.BytesRendered = resultStream.Length; - - return new RetrieveRenderedResponse(resultStream, resultStream.Length, outputContentType); - } - - catch (DataStoreException e) - { - // Log request details associated with exception. Note that the details are not for the store call that failed but for the request only. - _logger.LogError(e, "Error retrieving dicom resource to render"); - throw; - } - - } - - private async Task ConvertToImage(DicomFile dicomFile, int frameNumber, string mediaType, int quality, CancellationToken cancellationToken) - { - try - { - DicomImage dicomImage = new DicomImage(dicomFile.Dataset); - using var img = dicomImage.RenderImage(frameNumber); - using var sharpImage = img.AsSharpImage(); - MemoryStream resultStream = _recyclableMemoryStreamManager.GetStream(tag: nameof(ConvertToImage)); - - if (mediaType.Equals(KnownContentTypes.ImageJpeg, StringComparison.OrdinalIgnoreCase)) - { - JpegEncoder jpegEncoder = new JpegEncoder(); - jpegEncoder.Quality = quality; - await sharpImage.SaveAsJpegAsync(resultStream, jpegEncoder, cancellationToken: cancellationToken); - } - else - { - await sharpImage.SaveAsPngAsync(resultStream, new SixLabors.ImageSharp.Formats.Png.PngEncoder(), cancellationToken: cancellationToken); - } - - resultStream.Position = 0; - - return resultStream; - } - catch (Exception e) - { - _logger.LogError(e, "Error rendering dicom file into {OutputConentType} media type", mediaType); - throw new DicomImageException(); - } - } - - private static AcceptHeader GetValidRenderAcceptHeader(IReadOnlyCollection acceptHeaders) - { - EnsureArg.IsNotNull(acceptHeaders, nameof(acceptHeaders)); - - if (acceptHeaders.Count > 1) - { - throw new NotAcceptableException(DicomCoreResource.MultipleAcceptHeadersNotSupported); - } - - if (acceptHeaders.Count == 1) - { - var mediaType = acceptHeaders.First()?.MediaType; - - if (mediaType == null || (!StringSegment.Equals(mediaType.ToString(), KnownContentTypes.ImageJpeg, StringComparison.InvariantCultureIgnoreCase) && !StringSegment.Equals(mediaType.ToString(), KnownContentTypes.ImagePng, StringComparison.InvariantCultureIgnoreCase))) - { - throw new NotAcceptableException(DicomCoreResource.NotAcceptableHeaders); - } - - if (StringSegment.Equals(mediaType.ToString(), KnownContentTypes.ImagePng, StringComparison.InvariantCultureIgnoreCase)) - { - return new AcceptHeader(KnownContentTypes.ImagePng, PayloadTypes.SinglePart); - } - } - - return new AcceptHeader(KnownContentTypes.ImageJpeg, PayloadTypes.SinglePart); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/RetrieveResourceHandler.cs b/src/Microsoft.Health.Dicom.Core/Features/Retrieve/RetrieveResourceHandler.cs deleted file mode 100644 index a0d5345a28..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/RetrieveResourceHandler.cs +++ /dev/null @@ -1,52 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using MediatR; -using Microsoft.Health.Core.Features.Security.Authorization; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Security; -using Microsoft.Health.Dicom.Core.Messages; -using Microsoft.Health.Dicom.Core.Messages.Retrieve; - -namespace Microsoft.Health.Dicom.Core.Features.Retrieve; - -public class RetrieveResourceHandler : BaseHandler, IRequestHandler -{ - private readonly IRetrieveResourceService _retrieveResourceService; - - public RetrieveResourceHandler(IAuthorizationService authorizationService, IRetrieveResourceService retrieveResourceService) - : base(authorizationService) - { - _retrieveResourceService = EnsureArg.IsNotNull(retrieveResourceService, nameof(retrieveResourceService)); - } - - public async Task Handle( - RetrieveResourceRequest request, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(request, nameof(request)); - - if (await AuthorizationService.CheckAccess(DataActions.Read, cancellationToken) != DataActions.Read) - { - throw new UnauthorizedDicomActionException(DataActions.Read); - } - - ValidateRetrieveResourceRequest(request); - - return await _retrieveResourceService.GetInstanceResourceAsync(request, cancellationToken); - } - - private static void ValidateRetrieveResourceRequest(RetrieveResourceRequest request) - { - RetrieveRequestValidator.ValidateInstanceIdentifiers(request.ResourceType, request.StudyInstanceUid, request.SeriesInstanceUid, request.SopInstanceUid); - if (request.ResourceType == ResourceType.Frames) - { - RetrieveRequestValidator.ValidateFrames(request.Frames); - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/RetrieveResourceService.cs b/src/Microsoft.Health.Dicom.Core/Features/Retrieve/RetrieveResourceService.cs deleted file mode 100644 index 7734a04c05..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/RetrieveResourceService.cs +++ /dev/null @@ -1,419 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Context; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Core.Features.Telemetry; -using Microsoft.Health.Dicom.Core.Messages; -using Microsoft.Health.Dicom.Core.Messages.Retrieve; - -namespace Microsoft.Health.Dicom.Core.Features.Retrieve; - -public class RetrieveResourceService : IRetrieveResourceService -{ - private readonly IFileStore _blobDataStore; - private readonly IInstanceStore _instanceStore; - private readonly ITranscoder _transcoder; - private readonly IFrameHandler _frameHandler; - private readonly IAcceptHeaderHandler _acceptHeaderHandler; - private readonly IDicomRequestContextAccessor _dicomRequestContextAccessor; - private readonly IMetadataStore _metadataStore; - private readonly RetrieveConfiguration _retrieveConfiguration; - private readonly ILogger _logger; - private readonly IInstanceMetadataCache _instanceMetadataCache; - private readonly IFramesRangeCache _framesRangeCache; - private readonly RetrieveMeter _retrieveMeter; - - public RetrieveResourceService( - IInstanceStore instanceStore, - IFileStore blobDataStore, - ITranscoder transcoder, - IFrameHandler frameHandler, - IAcceptHeaderHandler acceptHeaderHandler, - IDicomRequestContextAccessor dicomRequestContextAccessor, - IMetadataStore metadataStore, - IInstanceMetadataCache instanceMetadataCache, - IFramesRangeCache framesRangeCache, - IOptionsSnapshot retrieveConfiguration, - RetrieveMeter retrieveMeter, - ILogger logger) - { - EnsureArg.IsNotNull(instanceStore, nameof(instanceStore)); - EnsureArg.IsNotNull(blobDataStore, nameof(blobDataStore)); - EnsureArg.IsNotNull(transcoder, nameof(transcoder)); - EnsureArg.IsNotNull(frameHandler, nameof(frameHandler)); - EnsureArg.IsNotNull(acceptHeaderHandler, nameof(acceptHeaderHandler)); - EnsureArg.IsNotNull(dicomRequestContextAccessor, nameof(dicomRequestContextAccessor)); - EnsureArg.IsNotNull(metadataStore, nameof(metadataStore)); - EnsureArg.IsNotNull(instanceMetadataCache, nameof(instanceMetadataCache)); - EnsureArg.IsNotNull(framesRangeCache, nameof(framesRangeCache)); - _retrieveMeter = EnsureArg.IsNotNull(retrieveMeter, nameof(retrieveMeter)); - EnsureArg.IsNotNull(logger, nameof(logger)); - EnsureArg.IsNotNull(retrieveConfiguration?.Value, nameof(retrieveConfiguration)); - - _instanceStore = instanceStore; - _blobDataStore = blobDataStore; - _transcoder = transcoder; - _frameHandler = frameHandler; - _acceptHeaderHandler = acceptHeaderHandler; - _dicomRequestContextAccessor = dicomRequestContextAccessor; - _metadataStore = metadataStore; - _retrieveConfiguration = retrieveConfiguration?.Value; - _logger = logger; - _instanceMetadataCache = instanceMetadataCache; - _framesRangeCache = framesRangeCache; - } - - public async Task GetInstanceResourceAsync(RetrieveResourceRequest message, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(message, nameof(message)); - var partition = _dicomRequestContextAccessor.RequestContext.GetPartition(); - - try - { - AcceptHeader validAcceptHeader = _acceptHeaderHandler.GetValidAcceptHeader( - message.ResourceType, - message.AcceptHeaders); - - string requestedTransferSyntax = validAcceptHeader.TransferSyntax.Value; - bool isOriginalTransferSyntaxRequested = DicomTransferSyntaxUids.IsOriginalTransferSyntaxRequested(requestedTransferSyntax); - - if (message.ResourceType == ResourceType.Frames) - { - return await GetFrameResourceAsync( - message, - partition, - requestedTransferSyntax, - isOriginalTransferSyntaxRequested, - validAcceptHeader.MediaType.ToString(), - validAcceptHeader.IsSinglePart, - cancellationToken); - } - - // this call throws NotFound when zero instance found - IEnumerable retrieveInstances = await _instanceStore.GetInstancesWithProperties( - message.ResourceType, partition, message.StudyInstanceUid, message.SeriesInstanceUid, message.SopInstanceUid, message.IsOriginalVersionRequested, cancellationToken); - InstanceMetadata instance = retrieveInstances.First(); - long version = instance.GetVersion(message.IsOriginalVersionRequested); - - bool needsTranscoding = NeedsTranscoding(isOriginalTransferSyntaxRequested, requestedTransferSyntax, instance); - - _dicomRequestContextAccessor.RequestContext.PartCount = retrieveInstances.Count(); - - // we will only support retrieving multiple instance if requested in original format, since we can do lazyStreams - if (retrieveInstances.Count() > 1 && !isOriginalTransferSyntaxRequested) - { - throw new NotAcceptableException( - string.Format(CultureInfo.CurrentCulture, DicomCoreResource.RetrieveServiceMultiInstanceTranscodingNotSupported, requestedTransferSyntax)); - } - - // transcoding of single instance - if (needsTranscoding) - { - FileProperties fileProperties = await RetrieveHelpers.CheckFileSize(_blobDataStore, _retrieveConfiguration.MaxDicomFileSize, version, partition, instance.InstanceProperties.FileProperties, false, _logger, cancellationToken); - LogFileSize(fileProperties.ContentLength, version, needsTranscoding, instance.InstanceProperties.HasFrameMetadata); - SetTranscodingBillingProperties(fileProperties.ContentLength); - - using Stream stream = await _blobDataStore.GetFileAsync(version, instance.VersionedInstanceIdentifier.Partition, instance.InstanceProperties.FileProperties, cancellationToken); - Stream transcodedStream = await _transcoder.TranscodeFileAsync(stream, requestedTransferSyntax); - - IAsyncEnumerable transcodedEnum = - GetTranscodedStreams( - isOriginalTransferSyntaxRequested, - transcodedStream, - instance, - requestedTransferSyntax) - .ToAsyncEnumerable(); - - return new RetrieveResourceResponse( - transcodedEnum, - validAcceptHeader.MediaType.ToString(), - validAcceptHeader.IsSinglePart); - } - - // no transcoding - IAsyncEnumerable responses = GetAsyncEnumerableStreams(retrieveInstances, isOriginalTransferSyntaxRequested, requestedTransferSyntax, message.IsOriginalVersionRequested, version, instance.InstanceProperties.HasFrameMetadata, cancellationToken); - return new RetrieveResourceResponse(responses, validAcceptHeader.MediaType.ToString(), validAcceptHeader.IsSinglePart); - } - catch (DataStoreException e) - { - // Log request details associated with exception. Note that the details are not for the store call that failed but for the request only. - _logger.LogError(e, "Error retrieving dicom resource. StudyInstanceUid: {StudyInstanceUid} SeriesInstanceUid: {SeriesInstanceUid} SopInstanceUid: {SopInstanceUid}", message.StudyInstanceUid, message.SeriesInstanceUid, message.SopInstanceUid); - - throw; - } - } - - private async Task GetFrameResourceAsync( - RetrieveResourceRequest message, - Partition partition, - string requestedTransferSyntax, - bool isOriginalTransferSyntaxRequested, - string mediaType, - bool isSinglePart, - CancellationToken cancellationToken) - { - - if (isSinglePart && message.Frames.Count > 1) - { - throw new BadRequestException(DicomCoreResource.SinglePartSupportedForSingleFrame); - } - - _dicomRequestContextAccessor.RequestContext.PartCount = message.Frames.Count; - - // only caching frames which are required to provide all 3 UIDs and more immutable - InstanceIdentifier instanceIdentifier = new InstanceIdentifier(message.StudyInstanceUid, message.SeriesInstanceUid, message.SopInstanceUid, partition); - string key = GenerateInstanceCacheKey(instanceIdentifier); - InstanceMetadata instance = await _instanceMetadataCache.GetAsync( - key, - instanceIdentifier, - GetInstanceMetadata, - cancellationToken); - - bool needsTranscoding = NeedsTranscoding(isOriginalTransferSyntaxRequested, requestedTransferSyntax, instance); - - // need the entire DicomDataset for transcoding - if (!needsTranscoding && instance.InstanceProperties.HasFrameMetadata) - { - _logger.LogInformation("Executing fast frame get."); - - // To get frame range metadata file, we use the original version of the instance, since we are not changing the pixel data - // else we use the current version. - long version = instance.InstanceProperties.OriginalVersion ?? instance.VersionedInstanceIdentifier.Version; - - // get frame range - IReadOnlyDictionary framesRange = await _framesRangeCache.GetAsync( - version, - version, - _metadataStore.GetInstanceFramesRangeAsync, - cancellationToken); - - string responseTransferSyntax = GetResponseTransferSyntax(isOriginalTransferSyntaxRequested, requestedTransferSyntax, instance); - - IAsyncEnumerable fastFrames = GetAsyncEnumerableFastFrameStreams( - version, - framesRange, - message.Frames, - responseTransferSyntax, - instance.InstanceProperties.FileProperties, - cancellationToken); - - return new RetrieveResourceResponse(fastFrames, mediaType, isSinglePart); - } - _logger.LogInformation("Downloading the entire instance for frame parsing"); - - // Get file properties again for transcoding - instance = await GetInstanceMetadata(instanceIdentifier, isInitialVersion: false, cancellationToken); - - FileProperties fileProperties = await RetrieveHelpers.CheckFileSize(_blobDataStore, _retrieveConfiguration.MaxDicomFileSize, instance.VersionedInstanceIdentifier.Version, partition, instance.InstanceProperties.FileProperties, render: false, _logger, cancellationToken); - LogFileSize(fileProperties.ContentLength, instance.VersionedInstanceIdentifier.Version, needsTranscoding, instance.InstanceProperties.HasFrameMetadata); - - // eagerly doing getFrames to validate frame numbers are valid before returning a response - Stream stream = await _blobDataStore.GetFileAsync(instance.VersionedInstanceIdentifier.Version, partition, instance.InstanceProperties.FileProperties, cancellationToken); - IReadOnlyCollection frameStreams = await _frameHandler.GetFramesResourceAsync( - stream, - message.Frames, - isOriginalTransferSyntaxRequested, - requestedTransferSyntax); - - if (needsTranscoding) - { - SetTranscodingBillingProperties(frameStreams.Sum(f => f.Length)); - } - - IAsyncEnumerable frames = GetAsyncEnumerableFrameStreams( - frameStreams, - instance, - isOriginalTransferSyntaxRequested, - requestedTransferSyntax); - - return new RetrieveResourceResponse(frames, mediaType, isSinglePart); - - } - - private void LogFileSize(long size, long version, bool needsTranscoding, bool hasFrameMetadata = false) - { - _logger.LogInformation( - "Retrieving Instance for watermark {Watermark} of size {ContentLength}, isTranscoding is {NeedsTranscoding}", - version, size, needsTranscoding); - _retrieveMeter.RetrieveInstanceCount.Add( - size, - RetrieveMeter.RetrieveInstanceCountTelemetryDimension(isTranscoding: needsTranscoding, hasFrameMetadata: hasFrameMetadata)); - } - - private void SetTranscodingBillingProperties(long bytesTranscoded) - { - _dicomRequestContextAccessor.RequestContext.IsTranscodeRequested = true; - _dicomRequestContextAccessor.RequestContext.BytesTranscoded = bytesTranscoded; - } - - private static string GetResponseTransferSyntax(bool isOriginalTransferSyntaxRequested, string requestedTransferSyntax, InstanceMetadata instanceMetadata) - { - if (isOriginalTransferSyntaxRequested) - { - return GetOriginalTransferSyntaxWithBackCompat(requestedTransferSyntax, instanceMetadata); - } - return requestedTransferSyntax; - } - - /// - /// Existing dicom files(as of Feb 2022) do not have transferSyntax stored. - /// Untill we backfill those files, we need this existing buggy fall back code: requestedTransferSyntax can be "*" which is the wrong content-type to return - /// - /// - /// - /// - private static string GetOriginalTransferSyntaxWithBackCompat(string requestedTransferSyntax, InstanceMetadata instanceMetadata) - { - return string.IsNullOrEmpty(instanceMetadata.InstanceProperties.TransferSyntaxUid) ? requestedTransferSyntax : instanceMetadata.InstanceProperties.TransferSyntaxUid; - } - - private static bool NeedsTranscoding(bool isOriginalTransferSyntaxRequested, string requestedTransferSyntax, InstanceMetadata instanceMetadata) - { - if (isOriginalTransferSyntaxRequested) - return false; - - return !(instanceMetadata.InstanceProperties.TransferSyntaxUid != null - && DicomTransferSyntaxUids.AreEqual(requestedTransferSyntax, instanceMetadata.InstanceProperties.TransferSyntaxUid)); - } - - private async IAsyncEnumerable GetAsyncEnumerableStreams( - IEnumerable instanceMetadataList, - bool isOriginalTransferSyntaxRequested, - string requestedTransferSyntax, - bool isOriginalVersionRequested, - long requestedVersion, - bool hasFrameMetadata, - [EnumeratorCancellation] CancellationToken cancellationToken) - { - long streamTotalLength = 0; - foreach (var instanceMetadata in instanceMetadataList) - { - long version = instanceMetadata.GetVersion(isOriginalVersionRequested); - FileProperties fileProperties = await _blobDataStore.GetFilePropertiesAsync(version, _dicomRequestContextAccessor.RequestContext.GetPartition(), instanceMetadata.InstanceProperties.FileProperties, cancellationToken); - Stream stream = await _blobDataStore.GetStreamingFileAsync(version, _dicomRequestContextAccessor.RequestContext.GetPartition(), instanceMetadata.InstanceProperties.FileProperties, cancellationToken); - streamTotalLength += fileProperties.ContentLength; - yield return - new RetrieveResourceInstance( - stream, - GetResponseTransferSyntax(isOriginalTransferSyntaxRequested, requestedTransferSyntax, instanceMetadata), - fileProperties.ContentLength); - } - LogFileSize(streamTotalLength, requestedVersion, needsTranscoding: false, hasFrameMetadata: hasFrameMetadata); - } - - private static async IAsyncEnumerable GetAsyncEnumerableFrameStreams( - IEnumerable frameStreams, - InstanceMetadata instanceMetadata, - bool isOriginalTransferSyntaxRequested, - string requestedTransferSyntax) - { - // fake await to return AsyncEnumerable and keep the response consistent - await Task.Run(() => 1); - // responseTransferSyntax is same for all frames in a instance - var responseTransferSyntax = GetResponseTransferSyntax(isOriginalTransferSyntaxRequested, requestedTransferSyntax, instanceMetadata); - foreach (Stream frameStream in frameStreams) - { - yield return - new RetrieveResourceInstance(frameStream, responseTransferSyntax, frameStream.Length); - } - } - - private static IEnumerable GetTranscodedStreams( - bool isOriginalTransferSyntaxRequested, - Stream transcodedStream, - InstanceMetadata instanceMetadata, - string requestedTransferSyntax) - { - yield return new RetrieveResourceInstance(transcodedStream, GetResponseTransferSyntax(isOriginalTransferSyntaxRequested, requestedTransferSyntax, instanceMetadata), transcodedStream.Length); - } - - private async IAsyncEnumerable GetAsyncEnumerableFastFrameStreams( - long version, - IReadOnlyDictionary framesRange, - IReadOnlyCollection frames, - string responseTransferSyntax, - FileProperties fileProperties, - [EnumeratorCancellation] CancellationToken cancellationToken) - { - long streamTotalLength = 0; - // eager validation before yield return - foreach (int frame in frames) - { - if (!framesRange.TryGetValue(frame, out FrameRange newFrameRange)) - throw new FrameNotFoundException(); - } - - foreach (int frame in frames) - { - FrameRange frameRange = framesRange[frame]; - Stream frameStream = await _blobDataStore.GetFileFrameAsync(version, _dicomRequestContextAccessor.RequestContext.GetPartition(), frameRange, fileProperties, cancellationToken); - streamTotalLength += frameRange.Length; - - yield return new RetrieveResourceInstance(frameStream, responseTransferSyntax, frameRange.Length); - } - LogFileSize(streamTotalLength, version, needsTranscoding: false, hasFrameMetadata: true); - } - - private static string GenerateInstanceCacheKey(InstanceIdentifier instanceIdentifier) - { - return $"{instanceIdentifier.Partition.Key}/{instanceIdentifier.StudyInstanceUid}/{instanceIdentifier.SeriesInstanceUid}/{instanceIdentifier.SopInstanceUid}"; - } - - private async Task GetInstanceMetadata(InstanceIdentifier instanceIdentifier, CancellationToken cancellationToken) - { - var partition = new Partition(instanceIdentifier.Partition.Key, instanceIdentifier.Partition.Name); - IEnumerable retrieveInstances = await _instanceStore.GetInstancesWithProperties( - ResourceType.Instance, - partition, - instanceIdentifier.StudyInstanceUid, - instanceIdentifier.SeriesInstanceUid, - instanceIdentifier.SopInstanceUid, - isInitialVersion: true, // Setting the flag to default true. For update we will always use the initial version. - cancellationToken); - - if (!retrieveInstances.Any()) - { - throw new InstanceNotFoundException(); - } - - return retrieveInstances.First(); - } - - private async Task GetInstanceMetadata(InstanceIdentifier instanceIdentifier, bool isInitialVersion, CancellationToken cancellationToken) - { - var partition = new Partition(instanceIdentifier.Partition.Key, instanceIdentifier.Partition.Name); - IEnumerable retrieveInstances = await _instanceStore.GetInstancesWithProperties( - ResourceType.Instance, - partition, - instanceIdentifier.StudyInstanceUid, - instanceIdentifier.SeriesInstanceUid, - instanceIdentifier.SopInstanceUid, - isInitialVersion, - cancellationToken); - - if (!retrieveInstances.Any()) - { - throw new InstanceNotFoundException(); - } - - return retrieveInstances.First(); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/Transcoder.cs b/src/Microsoft.Health.Dicom.Core/Features/Retrieve/Transcoder.cs deleted file mode 100644 index 5db11501ed..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Retrieve/Transcoder.cs +++ /dev/null @@ -1,145 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.IO; -using System.Threading.Tasks; -using EnsureThat; -using FellowOakDicom; -using FellowOakDicom.Imaging; -using FellowOakDicom.Imaging.Codec; -using FellowOakDicom.IO.Buffer; -using Microsoft.Extensions.Logging; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.IO; - -namespace Microsoft.Health.Dicom.Core.Features.Retrieve; - -public class Transcoder : ITranscoder -{ - private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager; - private readonly ILogger _logger; - private static readonly Action LogTranscodingFrameErrorDelegate = LoggerMessage.Define( - logLevel: LogLevel.Error, - eventId: default, - formatString: "Failed to transcode frame {FrameIndex} from {InputTransferSyntax} to {OutputTransferSyntax}"); - - private static readonly Action LogTranscodingFileErrorDelegate = LoggerMessage.Define( - logLevel: LogLevel.Error, - eventId: default, - formatString: "Failed to transcode dicom file from {InputTransferSyntax} to {OutputTransferSyntax}"); - - public Transcoder(RecyclableMemoryStreamManager recyclableMemoryStreamManager, ILogger logger) - { - _recyclableMemoryStreamManager = EnsureArg.IsNotNull(recyclableMemoryStreamManager, nameof(recyclableMemoryStreamManager)); - _logger = EnsureArg.IsNotNull(logger, nameof(logger)); - } - - public async Task TranscodeFileAsync(Stream stream, string requestedTransferSyntax) - { - EnsureArg.IsNotNull(stream, nameof(stream)); - EnsureArg.IsNotEmptyOrWhiteSpace(requestedTransferSyntax, nameof(requestedTransferSyntax)); - var parsedDicomTransferSyntax = DicomTransferSyntax.Parse(requestedTransferSyntax); - - DicomFile dicomFile; - - try - { - dicomFile = await DicomFile.OpenAsync(stream, FileReadOption.ReadLargeOnDemand); - } - catch (DicomFileException) - { - throw; - } - - stream.Seek(0, SeekOrigin.Begin); - - return await TranscodeFileAsync(dicomFile, parsedDicomTransferSyntax); - } - - public Stream TranscodeFrame(DicomFile dicomFile, int frameIndex, string requestedTransferSyntax) - { - EnsureArg.IsNotNull(dicomFile, nameof(dicomFile)); - EnsureArg.IsNotEmptyOrWhiteSpace(requestedTransferSyntax, nameof(requestedTransferSyntax)); - DicomDataset dataset = dicomFile.Dataset; - - // Validate requested frame index exists in file. - dicomFile.GetPixelDataAndValidateFrames(new[] { frameIndex }); - var parsedDicomTransferSyntax = DicomTransferSyntax.Parse(requestedTransferSyntax); - - IByteBuffer resultByteBuffer = TranscodeFrame(dataset, frameIndex, parsedDicomTransferSyntax); - return _recyclableMemoryStreamManager.GetStream(nameof(TranscodeFrame), resultByteBuffer.Data, 0, resultByteBuffer.Data.Length); - } - - private IByteBuffer TranscodeFrame(DicomDataset dataset, int frameIndex, DicomTransferSyntax targetSyntax) - { - // DicomTranscoder doesn't support transcoding frame(s), one workaround is to transcode entire dicomdataset, and return required frame, which would be inefficent when there are multiple frames (imaging 100 frames in one dataset). - // A better workaround is to create a dataset including only the required frame, and transcode it. - try - { - _logger.LogInformation("Transcoding frame from {InputTransferSyntax} to {OutputTransferSyntax}", dataset.InternalTransferSyntax?.UID?.UID, targetSyntax?.UID?.UID); - - DicomDataset datasetForFrame = CreateDatasetForFrame(dataset, frameIndex); - var transcoder = new DicomTranscoder(dataset.InternalTransferSyntax, targetSyntax); - DicomDataset result = transcoder.Transcode(datasetForFrame); - return DicomPixelData.Create(result).GetFrame(0); - } - catch (Exception ex) - { - LogTranscodingFrameErrorDelegate(_logger, frameIndex, dataset?.InternalTransferSyntax?.UID?.UID, targetSyntax?.UID?.UID, ex); - throw new TranscodingException(); - } - } - - private static DicomDataset CreateDatasetForFrame(DicomDataset dataset, int frameIndex) - { - IByteBuffer frameData = DicomPixelData.Create(dataset).GetFrame(frameIndex); - DicomDataset newDataset = dataset.Clone(); - var newdata = DicomPixelData.Create(newDataset, true); - newdata.AddFrame(frameData); - return newDataset; - } - - private async Task TranscodeFileAsync(DicomFile dicomFile, DicomTransferSyntax requestedTransferSyntax) - { - _logger.LogInformation("Transcoding instance from {InputTransferSyntax} to {OutputTransferSyntax}", dicomFile?.Dataset?.InternalTransferSyntax?.UID?.UID, requestedTransferSyntax?.UID?.UID); - - try - { - var transcoder = new DicomTranscoder( - dicomFile.Dataset.InternalTransferSyntax, - requestedTransferSyntax); - dicomFile = transcoder.Transcode(dicomFile); - } - catch (Exception ex) - { - LogTranscodingFileErrorDelegate(_logger, dicomFile?.Dataset?.InternalTransferSyntax?.UID?.UID, requestedTransferSyntax?.UID?.UID, ex); - - // TODO: Reevaluate this while fixing transcoding handling. - // We catch all here as Transcoder can throw a wide variety of things. - // Basically this means codec failure - a quite extraordinary situation, but not impossible - // Proper solution here would be to actually try transcoding all the files that we are - // returning and either form a PartialContent or NotAcceptable response with an extra error message in - // the headers. Because transcoding is an expensive operation, we choose to do it from within the - // LazyTransformReadOnlyStream at the time when response is being formed by the server, therefore this code - // is called from ASP.NET framework and at this point we can not change our server response. - // The decision for now is just to return an empty stream here letting the client handle it. - // In the future a more optimal solution may involve maintaining a cache of transcoded images and - // using that to determine if transcoding is possible from within the Handle method. - - throw new TranscodingException(); - } - - RecyclableMemoryStream resultStream = _recyclableMemoryStreamManager.GetStream(tag: nameof(TranscodeFileAsync)); - - if (dicomFile != null) - { - await dicomFile.SaveAsync(resultStream); - resultStream.Seek(offset: 0, loc: SeekOrigin.Begin); - } - - return resultStream; - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Routing/IUrlResolver.cs b/src/Microsoft.Health.Dicom.Core/Features/Routing/IUrlResolver.cs deleted file mode 100644 index 18ecea8b5d..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Routing/IUrlResolver.cs +++ /dev/null @@ -1,58 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using Microsoft.Health.Dicom.Core.Features.Model; - -namespace Microsoft.Health.Dicom.Core.Features.Routing; - -/// -/// Represents a utility for creating URLs for the DICOM service. -/// -public interface IUrlResolver -{ - /// - /// Resolves the URI for retrieving the status of an operation. - /// - /// The unique ID for a long-running operation. - /// An instance of pointing to where the status can be retrieved. - Uri ResolveOperationStatusUri(Guid operationId); - - /// - /// Resolves the URI for retrieving an extended query tag. - /// - /// The extended query tag path. - /// An instance of pointing to where the extended query tag can be retrieved. - Uri ResolveQueryTagUri(string tagPath); - - /// - /// Resolves the URI for retrieving the errors for an extended query tag. - /// - /// The extended query tag path. - /// An instance of pointing to where the extended query tag errors can be retrieved. - Uri ResolveQueryTagErrorsUri(string tagPath); - - /// - /// Resolves the URI to retrieve a study. - /// - /// The StudyInstanceUID. - /// An instance of pointing to where the study can be retrieved. - Uri ResolveRetrieveStudyUri(string studyInstanceUid); - - /// - /// Resovles the URI to retrieve an instance. - /// - /// The identifier to the instance. - /// Whether or not partition feature is enabled - /// An instance of pointing to where the instance can be retrieved. - Uri ResolveRetrieveInstanceUri(InstanceIdentifier instanceIdentifier, bool isPartitionEnabled); - - /// - /// Resovles the URI to retrieve a workitem instance. - /// - /// The identifier to the workitem instance - /// - Uri ResolveRetrieveWorkitemUri(string workitemInstanceUid); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Security/DataActions.cs b/src/Microsoft.Health.Dicom.Core/Features/Security/DataActions.cs deleted file mode 100644 index 170e14e961..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Security/DataActions.cs +++ /dev/null @@ -1,36 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.Serialization; - -namespace Microsoft.Health.Dicom.Core.Features.Security; - -[Flags] -[SuppressMessage("Design", "CA1028:Enum Storage should be Int32", Justification = "Reserve additional bits for actions.")] -public enum DataActions : ulong -{ - [EnumMember(Value = "none")] - None = 0, - - [EnumMember(Value = "read")] - Read = 1, - - [EnumMember(Value = "write")] - Write = 1 << 1, - - [EnumMember(Value = "delete")] - Delete = 1 << 2, - - [EnumMember(Value = "manageExtendedQueryTags")] - ManageExtendedQueryTags = 1 << 3, - - [EnumMember(Value = "export")] - Export = 1 << 4, - - [EnumMember(Value = "*")] - All = (Export << 1) - 1, -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Security/DicomRoleLoader.cs b/src/Microsoft.Health.Dicom.Core/Features/Security/DicomRoleLoader.cs deleted file mode 100644 index 700f036f7a..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Security/DicomRoleLoader.cs +++ /dev/null @@ -1,52 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.Serialization; -using EnsureThat; -using Microsoft.Extensions.Hosting; -using Microsoft.Health.Core.Configs; -using Microsoft.Health.Core.Features.Security; - -namespace Microsoft.Health.Dicom.Core.Features.Security; - -public class DicomRoleLoader : RoleLoader -{ - private readonly Dictionary _dataActionsMap = new Dictionary(); - - public DicomRoleLoader(AuthorizationConfiguration authorizationConfiguration, IHostEnvironment hostEnvironment) - : base(authorizationConfiguration, hostEnvironment) - { - // Loop through all the enums and pre-load them into a dictionary for mapping between string representation and enum - var enumType = typeof(DataActions); - foreach (var name in Enum.GetNames(enumType)) - { - var enumMemberAttribute = ((EnumMemberAttribute[])enumType.GetField(name).GetCustomAttributes(typeof(EnumMemberAttribute), true)).Single(); - _dataActionsMap.TryAdd(enumMemberAttribute.Value, Enum.Parse(name)); - } - } - - protected override Role RoleContractToRole(RoleContract roleContract) - { - EnsureArg.IsNotNull(roleContract, nameof(roleContract)); - - DataActions dataActions = roleContract.DataActions.Aggregate(DataActions.None, (acc, a) => acc | ToEnum(a)); - DataActions notDataActions = roleContract.NotDataActions.Aggregate(DataActions.None, (acc, a) => acc | ToEnum(a)); - - return new Role(roleContract.Name, dataActions & ~notDataActions, roleContract.Scopes.Single()); - } - - private DataActions ToEnum(string str) - { - if (_dataActionsMap.TryGetValue(str, out DataActions foundDataAction)) - { - return foundDataAction; - } - - throw new ArgumentOutOfRangeException($"Invalid data action supplied: {str}"); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Security/PrincipalClaimsExtractor.cs b/src/Microsoft.Health.Dicom.Core/Features/Security/PrincipalClaimsExtractor.cs deleted file mode 100644 index b4b89c3cba..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Security/PrincipalClaimsExtractor.cs +++ /dev/null @@ -1,37 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Linq; -using EnsureThat; -using Microsoft.Extensions.Options; -using Microsoft.Health.Core.Features.Security; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Features.Context; - -namespace Microsoft.Health.Dicom.Core.Features.Security; - -public class PrincipalClaimsExtractor : IClaimsExtractor -{ - private readonly IDicomRequestContextAccessor _dicomRequestContextAccessor; - private readonly SecurityConfiguration _securityConfiguration; - - public PrincipalClaimsExtractor(IDicomRequestContextAccessor dicomRequestContextAccessor, IOptions securityConfiguration) - { - EnsureArg.IsNotNull(dicomRequestContextAccessor, nameof(dicomRequestContextAccessor)); - EnsureArg.IsNotNull(securityConfiguration, nameof(securityConfiguration)); - - _dicomRequestContextAccessor = dicomRequestContextAccessor; - _securityConfiguration = securityConfiguration.Value; - } - - public IReadOnlyCollection> Extract() - { - return _dicomRequestContextAccessor.RequestContext.Principal?.Claims? - .Where(c => _securityConfiguration.PrincipalClaims?.Contains(c.Type) ?? false) - .Select(c => new KeyValuePair(c.Type, c.Value)) - .ToList(); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Security/roles.schema.json b/src/Microsoft.Health.Dicom.Core/Features/Security/roles.schema.json deleted file mode 100644 index 47179c90a9..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Security/roles.schema.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "http://microsoft.com/dicom/roles.schema.json", - "definitions": { - "dataActions": { - "enum": [ - "*", - "read", - "write", - "delete" - ] - } - }, - "type": "object", - "required": [ - "roles" - ], - "properties": { - "roles": { - "type": "array", - "items": { - "type": "object", - "required": [ - "name", - "dataActions", - "notDataActions", - "scopes" - ], - "properties": { - "name": { - "type": "string", - "examples": [ - "globalReader" - ], - "pattern": "^([a-zA-Z-_]+)$" - }, - "dataActions": { - "type": "array", - "items": { - "$ref": "#/definitions/dataActions" - } - }, - "notDataActions": { - "type": "array", - "items": { - "$ref": "#/definitions/dataActions" - } - }, - "scopes": { - "type": "array", - "items": { - "$id": "#/items/properties/scopes/items", - "const": "/" - }, - "minItems": 1, - "maxItems": 1 - } - }, - "additionalProperties": false - } - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Store/DatasetValidationException.cs b/src/Microsoft.Health.Dicom.Core/Features/Store/DatasetValidationException.cs deleted file mode 100644 index d6b4821179..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Store/DatasetValidationException.cs +++ /dev/null @@ -1,38 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Exceptions; - -namespace Microsoft.Health.Dicom.Core.Features.Store; - -/// -/// Exception thrown when the validation fails. -/// -public class DatasetValidationException : ValidationException -{ - public DatasetValidationException(ushort failureCode, string message) - : this(failureCode, message, innerException: null) - { - } - - public DatasetValidationException(ushort failureCode, string message, Exception innerException) - : base(message, innerException) - { - FailureCode = failureCode; - } - - public DatasetValidationException(ushort failureCode, string message, DicomTag dicomTag) - : this(failureCode, message, innerException: null) - { - FailureCode = failureCode; - DicomTag = dicomTag; - } - - public ushort FailureCode { get; } - - public DicomTag DicomTag { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Store/Entries/DicomInstanceEntryReaderForMultipartRequest.cs b/src/Microsoft.Health.Dicom.Core/Features/Store/Entries/DicomInstanceEntryReaderForMultipartRequest.cs deleted file mode 100644 index 323a081bec..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Store/Entries/DicomInstanceEntryReaderForMultipartRequest.cs +++ /dev/null @@ -1,100 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Net.Http.Headers; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.Extensions.Logging; -using Microsoft.Health.Abstractions.Exceptions; -using Microsoft.Health.Dicom.Core.Web; - -namespace Microsoft.Health.Dicom.Core.Features.Store.Entries; - -/// -/// Provides functionality to read DICOM instance entries from HTTP multipart request. -/// -public class DicomInstanceEntryReaderForMultipartRequest : IDicomInstanceEntryReader -{ - private readonly IMultipartReaderFactory _multipartReaderFactory; - private readonly ILogger _logger; - - public DicomInstanceEntryReaderForMultipartRequest( - IMultipartReaderFactory multipartReaderFactory, - ILogger logger) - { - EnsureArg.IsNotNull(multipartReaderFactory, nameof(multipartReaderFactory)); - EnsureArg.IsNotNull(logger, nameof(logger)); - - _multipartReaderFactory = multipartReaderFactory; - _logger = logger; - } - - /// - public bool CanRead(string contentType) - { - return MediaTypeHeaderValue.TryParse(contentType, out MediaTypeHeaderValue media) && - string.Equals(KnownContentTypes.MultipartRelated, media.MediaType, StringComparison.OrdinalIgnoreCase); - } - - /// - public async Task> ReadAsync(string contentType, Stream stream, CancellationToken cancellationToken) - { - EnsureArg.IsNotNullOrWhiteSpace(contentType, nameof(contentType)); - EnsureArg.IsNotNull(stream, nameof(stream)); - - IMultipartReader multipartReader = _multipartReaderFactory.Create(contentType, stream); - - var dicomInstanceEntries = new List(); - - MultipartBodyPart bodyPart; - - try - { - while ((bodyPart = await multipartReader.ReadNextBodyPartAsync(cancellationToken)) != null) - { - // Check the content type to make sure we can process. - if (!KnownContentTypes.ApplicationDicom.Equals(bodyPart.ContentType, StringComparison.OrdinalIgnoreCase)) - { - // TODO: Currently, we only support application/dicom. Support for metadata + bulkdata is coming. - throw new UnsupportedMediaTypeException( - string.Format(CultureInfo.InvariantCulture, DicomCoreResource.UnsupportedContentType, bodyPart.ContentType)); - } - - dicomInstanceEntries.Add(new StreamOriginatedDicomInstanceEntry(bodyPart.SeekableStream)); - } - } - catch (Exception) - { - // Encountered an error while processing, release all resources. - IEnumerable disposeTasks = dicomInstanceEntries.Select(DisposeResourceAsync); - - await Task.WhenAll(disposeTasks); - - throw; - } - - return dicomInstanceEntries; - } - - [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Ignore errors during disposal.")] - private async Task DisposeResourceAsync(IDicomInstanceEntry resource) - { - try - { - await resource.DisposeAsync(); - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to dispose the resource."); - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Store/Entries/DicomInstanceEntryReaderForSinglePartRequest.cs b/src/Microsoft.Health.Dicom.Core/Features/Store/Entries/DicomInstanceEntryReaderForSinglePartRequest.cs deleted file mode 100644 index c6bf6448cd..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Store/Entries/DicomInstanceEntryReaderForSinglePartRequest.cs +++ /dev/null @@ -1,193 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.IO; -using System.Net.Http.Headers; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Health.Abstractions.Exceptions; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Web; -using NotSupportedException = System.NotSupportedException; - -namespace Microsoft.Health.Dicom.Core.Features.Store.Entries; - -/// -/// Provides functionality to read DICOM instance entries from HTTP application/dicom request. -/// -public class DicomInstanceEntryReaderForSinglePartRequest : IDicomInstanceEntryReader -{ - private readonly ISeekableStreamConverter _seekableStreamConverter; - private readonly StoreConfiguration _storeConfiguration; - private readonly ILogger _logger; - - public DicomInstanceEntryReaderForSinglePartRequest(ISeekableStreamConverter seekableStreamConverter, IOptions storeConfiguration, ILogger logger) - { - _seekableStreamConverter = EnsureArg.IsNotNull(seekableStreamConverter, nameof(seekableStreamConverter)); - _storeConfiguration = EnsureArg.IsNotNull(storeConfiguration?.Value, nameof(storeConfiguration)); - _logger = EnsureArg.IsNotNull(logger, nameof(logger)); - } - - /// - public bool CanRead(string contentType) - { - return MediaTypeHeaderValue.TryParse(contentType, out MediaTypeHeaderValue media) && - string.Equals(KnownContentTypes.ApplicationDicom, media.MediaType, StringComparison.OrdinalIgnoreCase); - } - - /// - public async Task> ReadAsync(string contentType, Stream stream, CancellationToken cancellationToken) - { - EnsureArg.IsNotNullOrWhiteSpace(contentType, nameof(contentType)); - EnsureArg.IsNotNull(stream, nameof(stream)); - - var dicomInstanceEntries = new List(); - - if (!KnownContentTypes.ApplicationDicom.Equals(contentType, StringComparison.OrdinalIgnoreCase)) - { - // TODO: Currently, we only support application/dicom. Support for metadata + bulkdata is coming. - throw new UnsupportedMediaTypeException( - string.Format(CultureInfo.InvariantCulture, DicomCoreResource.UnsupportedContentType, contentType)); - } - - // Can dispose of the underlying stream becasue in seekableStreamConverter the entire stream is copied over into a new stream - Stream seekableStream; - using (Stream limitStream = new ReadOnlyLimitStream(stream, _storeConfiguration.MaxAllowedDicomFileSize)) - { - seekableStream = await _seekableStreamConverter.ConvertAsync(limitStream, cancellationToken); - } - - dicomInstanceEntries.Add(new StreamOriginatedDicomInstanceEntry(seekableStream)); - - return dicomInstanceEntries; - } - - private class ReadOnlyLimitStream : Stream - { - private readonly Stream _stream; - - private long _bytesLeft; - - private readonly long _limit; - - public override bool CanRead => _stream.CanRead; - - public override bool CanSeek => false; - - public override bool CanWrite => false; - - public override long Length => throw new NotSupportedException(); - - public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } - - public ReadOnlyLimitStream(Stream stream, long limit) - { - EnsureArg.IsNotNull(stream, nameof(stream)); - EnsureArg.IsGte(limit, 0); - - _stream = stream; - _bytesLeft = limit; - _limit = limit; - } - - public override void Flush() - { - _stream.Flush(); - } - - public override int ReadByte() - { - ThrowIfExceedLimit(); - - int read = _stream.ReadByte(); - - if (read != -1) - { - _bytesLeft--; - ThrowIfExceedLimit(); - } - - return read; - } - - public override int Read(byte[] buffer, int offset, int count) - { - ThrowIfExceedLimit(); - - int amountRead = _stream.Read(buffer, offset, count); - _bytesLeft -= amountRead; - - ThrowIfExceedLimit(); - - return amountRead; - } - - [SuppressMessage("Performance", "CA1835:Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync'", Justification = "Buffer is pass through")] - public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - ThrowIfExceedLimit(); - - int amountRead = await _stream.ReadAsync(buffer, offset, count, cancellationToken); - _bytesLeft -= amountRead; - - ThrowIfExceedLimit(); - - return amountRead; - } - - public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) - { - ThrowIfExceedLimit(); - - int amountRead = await _stream.ReadAsync(buffer, cancellationToken); - _bytesLeft -= amountRead; - - ThrowIfExceedLimit(); - - return amountRead; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void ThrowIfExceedLimit() - { - if (_bytesLeft < 0) - { - throw new PayloadTooLargeException(_limit); - } - } - - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotSupportedException(); - } - - public override void SetLength(long value) - { - throw new NotSupportedException(); - } - - public override void Write(byte[] buffer, int offset, int count) - { - _stream.Write(buffer, offset, count); - } - - protected override void Dispose(bool disposing) - { - if (disposing) - _stream.Dispose(); - - base.Dispose(disposing); - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Store/Entries/DicomInstanceEntryReaderManager.cs b/src/Microsoft.Health.Dicom.Core/Features/Store/Entries/DicomInstanceEntryReaderManager.cs deleted file mode 100644 index 4cf62a0c98..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Store/Entries/DicomInstanceEntryReaderManager.cs +++ /dev/null @@ -1,31 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Linq; -using EnsureThat; - -namespace Microsoft.Health.Dicom.Core.Features.Store.Entries; - -/// -/// Provides functionality to find the appropriate . -/// -public class DicomInstanceEntryReaderManager : IDicomInstanceEntryReaderManager -{ - private readonly IEnumerable _dicomInstanceEntryReaders; - - public DicomInstanceEntryReaderManager(IEnumerable dicomInstanceEntryReaders) - { - EnsureArg.IsNotNull(dicomInstanceEntryReaders, nameof(dicomInstanceEntryReaders)); - - _dicomInstanceEntryReaders = dicomInstanceEntryReaders; - } - - /// - public IDicomInstanceEntryReader FindReader(string contentType) - { - return _dicomInstanceEntryReaders.FirstOrDefault(reader => reader.CanRead(contentType)); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Store/Entries/IDicomInstanceEntry.cs b/src/Microsoft.Health.Dicom.Core/Features/Store/Entries/IDicomInstanceEntry.cs deleted file mode 100644 index b9b4f40e12..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Store/Entries/IDicomInstanceEntry.cs +++ /dev/null @@ -1,34 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Exceptions; - -namespace Microsoft.Health.Dicom.Core.Features.Store.Entries; - -/// -/// Represents a DICOM instance entry that's been read from the HTTP request body. -/// -public interface IDicomInstanceEntry : IAsyncDisposable -{ - /// - /// Gets the of the DICOM instance entry. - /// - /// The cancellation token. - /// An instance of . - /// Thrown when the DICOM instance entry is invalid. - ValueTask GetDicomDatasetAsync(CancellationToken cancellationToken); - - /// - /// Gets the of the DICOM instance entry. - /// - /// The cancellation token. - /// An instance of . - ValueTask GetStreamAsync(CancellationToken cancellationToken); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Store/Entries/IDicomInstanceEntryReader.cs b/src/Microsoft.Health.Dicom.Core/Features/Store/Entries/IDicomInstanceEntryReader.cs deleted file mode 100644 index c4522cfe04..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Store/Entries/IDicomInstanceEntryReader.cs +++ /dev/null @@ -1,33 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.Health.Dicom.Core.Features.Store.Entries; - -/// -/// Provides functionality to read DICOM instance entries from stream such as HTTP request body. -/// -public interface IDicomInstanceEntryReader -{ - /// - /// Gets a flag indicating whether this reader can read the HTTP request body with . - /// - /// The content type. - /// true if the reader can read the content; otherwise, false. - bool CanRead(string contentType); - - /// - /// Asynchronously reads the DICOM instance entries from the . - /// - /// The content type. - /// The stream to read the DICOM instances from. - /// The cancellation token. - /// A task represents the asynchronous read operation. - Task> ReadAsync(string contentType, Stream stream, CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Store/Entries/IDicomInstanceEntryReaderManager.cs b/src/Microsoft.Health.Dicom.Core/Features/Store/Entries/IDicomInstanceEntryReaderManager.cs deleted file mode 100644 index 4d93422024..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Store/Entries/IDicomInstanceEntryReaderManager.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Features.Store.Entries; - -/// -/// Provides functionality to find the appropriate . -/// -public interface IDicomInstanceEntryReaderManager -{ - /// - /// Finds the appropriate that can read . - /// - /// The content type. - /// An instance of if found; otherwise, null. - IDicomInstanceEntryReader FindReader(string contentType); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Store/Entries/StreamOriginatedDicomInstanceEntry.cs b/src/Microsoft.Health.Dicom.Core/Features/Store/Entries/StreamOriginatedDicomInstanceEntry.cs deleted file mode 100644 index 9994c667dc..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Store/Entries/StreamOriginatedDicomInstanceEntry.cs +++ /dev/null @@ -1,66 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Features.Common; - -namespace Microsoft.Health.Dicom.Core.Features.Store.Entries; - -/// -/// Represents a DICOM instance entry originated from stream. -/// -public sealed class StreamOriginatedDicomInstanceEntry : IDicomInstanceEntry -{ - private readonly Stream _stream; - private readonly AsyncCache _dicomFileCache; - private const int LargeObjectsizeInBytes = 1000; - - /// - /// Initializes a new instance of the class. - /// - /// The stream. - /// The must be seekable. - internal StreamOriginatedDicomInstanceEntry(Stream seekableStream) - { - // The stream must be seekable. - EnsureArg.IsNotNull(seekableStream, nameof(seekableStream)); - EnsureArg.IsTrue(seekableStream.CanSeek, nameof(seekableStream)); - - _stream = seekableStream; - _dicomFileCache = new AsyncCache(_ => DicomFile.OpenAsync(_stream, FileReadOption.ReadLargeOnDemand, largeObjectSize: LargeObjectsizeInBytes)); - } - - /// - public async ValueTask GetDicomDatasetAsync(CancellationToken cancellationToken) - { - try - { - DicomFile file = await _dicomFileCache.GetAsync(cancellationToken: cancellationToken); - return file.Dataset; - } - catch (DicomFileException) - { - throw new InvalidInstanceException(DicomCoreResource.InvalidDicomInstance); - } - } - - /// - public ValueTask GetStreamAsync(CancellationToken cancellationToken) - { - _stream.Seek(0, SeekOrigin.Begin); - return new ValueTask(_stream); - } - - public async ValueTask DisposeAsync() - { - _dicomFileCache.Dispose(); - await _stream.DisposeAsync(); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Store/ErrorNumbers.cs b/src/Microsoft.Health.Dicom.Core/Features/Store/ErrorNumbers.cs deleted file mode 100644 index 69c02787c0..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Store/ErrorNumbers.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Features.Store; - -/// -/// This class is meant to define error for DICOM failure so it becomes easier to search -/// -internal static class ErrorNumbers -{ - internal const int ValidationFailure = 100; // For next Error number increase the counter by 1 -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Store/FailureReasonCodes.cs b/src/Microsoft.Health.Dicom.Core/Features/Store/FailureReasonCodes.cs deleted file mode 100644 index c178e4d24b..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Store/FailureReasonCodes.cs +++ /dev/null @@ -1,132 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Features.Store; - -/// -/// If any of the failure codes are modified, please check they match the DICOM conformance statement. -/// -internal static class FailureReasonCodes -{ - /// - /// General exception in processing the DICOM instance. - /// - public const ushort ProcessingFailure = 272; - - /// - /// Data Set does not contain one or more required attributes. - /// - /// - /// - /// - public const ushort MissingAttribute = 288; - - /// - /// Data Set contains one or more attributes which are missing required values. - /// - /// - /// - /// - public const ushort MissingAttributeValue = 289; - - /// - /// The DICOM instance failed validation. - /// - public const ushort ValidationFailure = 43264; - - /// - /// The DICOM instance already exists. - /// - public const ushort SopInstanceAlreadyExists = 45070; - - /// - /// The DICOM instance is being created. - /// - public const ushort PendingSopInstance = 45071; - - /// - /// FAILURE - Specified SOP Instance UID does not exist or is not a UPS Instance managed by this SCP - /// - /// - /// Hex code are the defined ones in spec. - /// - /// - public const ushort UpsInstanceNotFound = 0xC307; - - /// - /// FAILURE - Refused: The UPS may no longer be updated - /// - /// - /// Hex code are the defined ones in spec. - /// - /// - public const ushort UpsInstanceUpdateNotAllowed = 0xC300; - - /// - /// FAILURE - Refused: The UPS is already COMPLETED - /// - /// - /// Hex code are the defined ones in spec. - /// - /// - public const ushort UpsIsAlreadyCompleted = 0xC306; - - /// - /// WARNING - The UPS is already in the requested state of CANCELED - /// - /// - /// Hex code are the defined ones in spec. - /// - /// - public const ushort UpsIsAlreadyCanceled = 0xC304; - - /// - /// FAILURE - Failed: Performer chooses not to cancel - /// - /// - /// Hex code are the defined ones in spec. - /// - /// - public const ushort UpsPerformerChoosesNotToCancel = 0xC313; - - /// - /// FAILURE - Refused: The UPS is not in the "IN PROGRESS" state - /// - /// - /// Hex code are the defined ones in spec. - /// - /// - public const ushort UpsNotInProgressState = 0xC310; - - /// - /// FAILURE - Refused: The correct Transaction UID was not provided - /// - /// - /// Hex code are the defined ones in spec. - /// - /// - public const ushort UpsTransactionUidIncorrect = 0xC301; - - /// - /// FAILURE - Refused: The Transaction UID was not provided - /// - public const ushort UpsTransactionUidAbsent = 0xC302; - - /// - /// FAILURE - Procedure step state is present in the dataset provided to be updated which is not allowed. - /// - public const ushort UpsProcedureStepStateNotAllowed = 0xC303; - - /// - /// FAILURE - The request is inconsistent with the current state of the Target Workitem. Please try again. - /// Usually happens when an update request updated the watermark before current request could finish. - /// - public const ushort UpsUpdateConflict = 0xC304; - - /// - /// General exception related to blob not found. - /// - public const ushort BlobNotFound = 273; -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Store/IIndexDataStore.cs b/src/Microsoft.Health.Dicom.Core/Features/Store/IIndexDataStore.cs deleted file mode 100644 index db41dc92db..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Store/IIndexDataStore.cs +++ /dev/null @@ -1,195 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Features.Partitioning; - -namespace Microsoft.Health.Dicom.Core.Features.Store; - -/// -/// Provides functionality to manage DICOM instance index. -/// -public interface IIndexDataStore -{ - /// - /// Asynchronously begins the addition of a DICOM instance. - /// - /// The partition. - /// The DICOM dataset to index. - /// Queryable dicom tags - /// The cancellation token. - /// A task that represents the asynchronous add operation. - Task BeginCreateInstanceIndexAsync(Partition partition, DicomDataset dicomDataset, IEnumerable queryTags, CancellationToken cancellationToken = default); - - /// - /// Asynchronously reindex a DICOM instance. - /// - /// The DICOM dataset to reindex. - /// The DICOM instance watermark. - /// Queryable dicom tags - /// The cancellation token. - /// A task that represents the asynchronous reindex operation. - Task ReindexInstanceAsync(DicomDataset dicomDataset, long watermark, IEnumerable queryTags, CancellationToken cancellationToken = default); - - /// - /// Asynchronously deletes the indices of all instances which belongs to the study specified by the , . - /// - /// The partition. - /// The StudyInstanceUID. - /// The date that the record can be cleaned up. - /// The cancellation token. - /// A task that represents the asynchronous delete operation. - Task> DeleteStudyIndexAsync(Partition partition, string studyInstanceUid, DateTimeOffset cleanupAfter, CancellationToken cancellationToken = default); - - /// - /// Asynchronously deletes the indices of all instances which belong to the series specified by the , and . - /// - /// The partition. - /// The StudyInstanceUID. - /// The SeriesInstanceUID. - /// The date that the record can be cleaned up. - /// The cancellation token. - /// A task that represents the asynchronous delete operation. - Task> DeleteSeriesIndexAsync(Partition partition, string studyInstanceUid, string seriesInstanceUid, DateTimeOffset cleanupAfter, CancellationToken cancellationToken = default); - - /// - /// Asynchronously deletes the indices of the instance specified by the , , , and . - /// - /// The partition. - /// The StudyInstanceUID. - /// The SeriesInstanceUID. - /// The SopInstanceUID. - /// The date that the record can be cleaned up. - /// The cancellation token. - /// A task that represents the asynchronous delete operation. - Task> DeleteInstanceIndexAsync(Partition partition, string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid, DateTimeOffset cleanupAfter, CancellationToken cancellationToken = default); - - /// - /// Asynchronously completes the addition of a DICOM instance. - /// - /// The partition key. - /// The DICOM dataset whose status should be updated. - /// The DICOM instance watermark - /// Queryable dicom tags - /// file properties - /// Optionally allow an out-of-date snapshot of . - /// Has additional frame range metadata stores. - /// The cancellation token. - /// A task that represents the asynchronous update operation. - Task EndCreateInstanceIndexAsync(int partitionKey, DicomDataset dicomDataset, long watermark, IEnumerable queryTags, FileProperties fileProperties = null, bool allowExpiredTags = false, bool hasFrameMetadata = false, CancellationToken cancellationToken = default); - - /// - /// Return a collection of deleted instances. - /// - /// The number of entries to return. - /// The maximum number of times a cleanup should be attempted. - /// The cancellation token. - /// A collection of deleted instances to cleanup. - Task> RetrieveDeletedInstancesAsync(int batchSize, int maxRetries, CancellationToken cancellationToken = default); - - /// - /// Return a collection of deleted instances with properties. - /// - /// The number of entries to return. - /// The maximum number of times a cleanup should be attempted. - /// The cancellation token. - /// A collection of deleted instances to cleanup. - Task> RetrieveDeletedInstancesWithPropertiesAsync(int batchSize, int maxRetries, CancellationToken cancellationToken = default); - - /// - /// Removes an item from the list of deleted entries that need to be cleaned up. - /// - /// The DICOM instance identifier. - /// The cancellation token. - /// A task that represents the asynchronous delete operation - Task DeleteDeletedInstanceAsync(VersionedInstanceIdentifier versionedInstanceIdentifier, CancellationToken cancellationToken = default); - - /// - /// Increments the retry count of a deleted instance. - /// - /// The DICOM instance identifier. - /// The date which cleanup can be attempted again - /// The cancellation token. - /// A task that represents the asynchronous update operation - Task IncrementDeletedInstanceRetryAsync(VersionedInstanceIdentifier versionedInstanceIdentifier, DateTimeOffset cleanupAfter, CancellationToken cancellationToken = default); - - /// - /// Retrieves the number of deleted instances which have reached the max number of retries. - /// - /// The max number of retries. - /// The cancellation token. - /// A task that gets the count - Task RetrieveNumExhaustedDeletedInstanceAttemptsAsync(int maxNumberOfRetries, CancellationToken cancellationToken = default); - - /// - /// Retrieves the of oldest instance waiting to be deleted - /// - /// The cancellation token. - /// A task that gets the date of the oldest deleted instance - Task GetOldestDeletedAsync(CancellationToken cancellationToken = default); - - /// - /// Asynchronously updates a DICOM instance NewWatermark - /// - /// The partition. - /// List of instances watermark to update - /// The cancellation token. - /// A task that with list of instance metadata with new watermark. - Task> BeginUpdateInstanceAsync(Partition partition, IReadOnlyCollection versions, CancellationToken cancellationToken = default); - - /// - /// Asynchronously updates a DICOM instance NewWatermark - /// - /// The partition. - /// StudyInstanceUID to update - /// The cancellation token. - /// A task that with list of instance metadata with new watermark. - Task> BeginUpdateInstancesAsync(Partition partition, string studyInstanceUid, CancellationToken cancellationToken = default); - - /// - /// Asynchronously bulk update all instances in a study, and update extendedquerytag with new watermark. - /// Also creates new changefeed entry - /// - /// The partition key. - /// - /// The DICOM dataset to index. - /// A list of instance metadata to use to update file properties for newly stored blob file from "update" - /// Queryable dicom tags - /// The cancellation token. - /// A task that represents the asynchronous add operation. - Task EndUpdateInstanceAsync(int partitionKey, string studyInstanceUid, DicomDataset dicomDataset, IReadOnlyList instanceMetadataList, IEnumerable queryTags, CancellationToken cancellationToken = default); - - /// - /// Asynchronously updates DICOM instance HasFrameMetadata to 1 - /// - /// The partition key. - /// List of instances watermark to update - /// Has additional frame range metadata stores. - /// The cancellation token. - /// A task that with list of instance metadata with new watermark. - Task UpdateFrameDataAsync(int partitionKey, IEnumerable versions, bool hasFrameMetadata, CancellationToken cancellationToken = default); - - /// - /// Asynchronously updates DICOM instance file properties content length - /// - /// file properties that need to get the content length updated - /// The cancellation token. - /// A task that with list of instance metadata with new watermark. - Task UpdateFilePropertiesContentLengthAsync(IReadOnlyDictionary filePropertiesByWatermark, CancellationToken cancellationToken = default); - - /// - /// Retrieves total count in FileProperty table and summation of all content length values across FileProperty table. - /// - /// The cancellation token. - /// A task that gets the count - Task GetIndexedFileMetricsAsync(CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Store/IIndexDataStoreExtensions.cs b/src/Microsoft.Health.Dicom.Core/Features/Store/IIndexDataStoreExtensions.cs deleted file mode 100644 index b74358e2a6..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Store/IIndexDataStoreExtensions.cs +++ /dev/null @@ -1,31 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.Health.Dicom.Core.Features.Model; - -namespace Microsoft.Health.Dicom.Core.Features.Store; - -public static class IIndexDataStoreExtensions -{ - /// - /// Deletes the instance index with no back off on cleaning up the underlying files. - /// - /// The IIndexDataStore. - /// The instance to delete. - /// The cancellation token. - /// A task representing the asynchronous delete command. - public static async Task DeleteInstanceIndexAsync(this IIndexDataStore indexDataStore, InstanceIdentifier instanceIdentifier, CancellationToken cancellationToken = default) - { - EnsureArg.IsNotNull(indexDataStore, nameof(indexDataStore)); - EnsureArg.IsNotNull(instanceIdentifier, nameof(instanceIdentifier)); - - await indexDataStore.DeleteInstanceIndexAsync( - instanceIdentifier.Partition, instanceIdentifier.StudyInstanceUid, instanceIdentifier.SeriesInstanceUid, instanceIdentifier.SopInstanceUid, DateTimeOffset.UtcNow, cancellationToken); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Store/IStoreDatasetValidator.cs b/src/Microsoft.Health.Dicom.Core/Features/Store/IStoreDatasetValidator.cs deleted file mode 100644 index 56640fac7e..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Store/IStoreDatasetValidator.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using FellowOakDicom; - -namespace Microsoft.Health.Dicom.Core.Features.Store; - -/// -/// Provides functionality to validate a to make sure it meets the requirement. -/// -public interface IStoreDatasetValidator -{ - /// - /// Validates the . - /// - /// The DICOM dataset to validate. - /// - /// If supplied, the StudyInstanceUID in the must match to be considered valid. - /// - /// The cancellation token - /// Thrown when the validation fails. - /// - /// ValidationWarnings. - /// - Task ValidateAsync(DicomDataset dicomDataset, string requiredStudyInstanceUid, CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Store/IStoreOrchestrator.cs b/src/Microsoft.Health.Dicom.Core/Features/Store/IStoreOrchestrator.cs deleted file mode 100644 index 3619b3a736..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Store/IStoreOrchestrator.cs +++ /dev/null @@ -1,29 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Health.Dicom.Core.Features.Store.Entries; - -namespace Microsoft.Health.Dicom.Core.Features.Store; - -/// -/// Provides functionality to orchestrate the storing of the DICOM instance entry. -/// -public interface IStoreOrchestrator -{ - /// - /// Asynchronously orchestrate the storing of a DICOM instance entry. - /// - /// The DICOM instance entry to store. - /// The cancellation token. - /// - /// A task that represents the asynchronous orchestration of the storing operation. - /// The value of the property is the length of the uploaded DICOM instance in bytes. - /// - Task StoreDicomInstanceEntryAsync( - IDicomInstanceEntry dicomInstanceEntry, - CancellationToken cancellationToken); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Store/IStoreResponseBuilder.cs b/src/Microsoft.Health.Dicom.Core/Features/Store/IStoreResponseBuilder.cs deleted file mode 100644 index 7e34203278..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Store/IStoreResponseBuilder.cs +++ /dev/null @@ -1,51 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Core.Messages.Store; - -namespace Microsoft.Health.Dicom.Core.Features.Store; - -/// -/// Provides functionality to build the response for the store transaction. -/// -public interface IStoreResponseBuilder -{ - /// - /// Builds the response. - /// - /// If specified and there is at least one success, then the RetrieveURL for the study will be set. - /// Whether to return 202 when warning set or not - /// An instance of representing the response. - StoreResponse BuildResponse(string studyInstanceUid, bool returnWarning202 = false); - - /// - /// Adds a Success entry to the response. - /// - /// The DICOM dataset that was successfully stored. - /// Store validation errors and warnings - /// Data Partition entry - /// The warning reason code. - /// Whether to build response warning sequence or not. - void AddSuccess(DicomDataset dicomDataset, - StoreValidationResult storeValidationResult, - Partition partition, - ushort? warningReasonCode = null, - bool buildWarningSequence = false); - - void SetWarningMessage(string message); - - /// - /// Adds a failed entry to the response. - /// - /// The DICOM dataset that failed to be stored. - /// The failure reason code. - /// Store validation errors and warnings - void AddFailure( - DicomDataset dicomDataset = null, - ushort failureReasonCode = FailureReasonCodes.ProcessingFailure, - StoreValidationResult storeValidationResult = null); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Store/IStoreService.cs b/src/Microsoft.Health.Dicom.Core/Features/Store/IStoreService.cs deleted file mode 100644 index c9915c9570..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Store/IStoreService.cs +++ /dev/null @@ -1,34 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Health.Dicom.Core.Features.Store.Entries; -using Microsoft.Health.Dicom.Core.Messages.Store; - -namespace Microsoft.Health.Dicom.Core.Features.Store; - -/// -/// Provides functionality to process the list of . -/// -public interface IStoreService -{ - /// - /// Asynchronously processes the . - /// - /// - /// If the is specified, every element in the - /// must have the given attribute value. - /// - /// The list of to process. - /// An optional value for the StudyInstanceUID tag. - /// The cancellation token. - /// A task that represents the asynchronous process operation. - Task ProcessAsync( - IReadOnlyList instanceEntries, - string requiredStudyInstanceUid, - CancellationToken cancellationToken); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Store/QueryTagsExpiredEventArgs.cs b/src/Microsoft.Health.Dicom.Core/Features/Store/QueryTagsExpiredEventArgs.cs deleted file mode 100644 index 1c53ee994a..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Store/QueryTagsExpiredEventArgs.cs +++ /dev/null @@ -1,18 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -namespace Microsoft.Health.Dicom.Core.Features.Store; - -public sealed class QueryTagsExpiredEventArgs : EventArgs -{ - public DicomDataset DicomDataset { get; set; } - - public IReadOnlyCollection NewQueryTags { get; set; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Store/StoreDatasetValidator.cs b/src/Microsoft.Health.Dicom.Core/Features/Store/StoreDatasetValidator.cs deleted file mode 100644 index 2a6ebe3f8e..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Store/StoreDatasetValidator.cs +++ /dev/null @@ -1,403 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Globalization; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using FellowOakDicom; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Context; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Features.Telemetry; -using Microsoft.Health.Dicom.Core.Features.Validation; - -namespace Microsoft.Health.Dicom.Core.Features.Store; - -/// -/// Provides functionality to validate a to make sure it meets the minimum requirement when storing. -/// -public class StoreDatasetValidator : IStoreDatasetValidator -{ - private readonly bool _enableFullDicomItemValidation; - private readonly IElementMinimumValidator _minimumValidator; - private readonly IQueryTagService _queryTagService; - private readonly StoreMeter _storeMeter; - private readonly IDicomRequestContextAccessor _dicomRequestContextAccessor; - private readonly ILogger _logger; - - private static readonly ImmutableHashSet RequiredCoreTags = ImmutableHashSet.Create( - DicomTag.StudyInstanceUID, - DicomTag.SeriesInstanceUID, - DicomTag.SOPInstanceUID, - DicomTag.PatientID, - DicomTag.SOPClassUID); - - private static readonly ImmutableHashSet RequiredV2CoreTags = ImmutableHashSet.Create( - DicomTag.StudyInstanceUID, - DicomTag.SeriesInstanceUID, - DicomTag.SOPInstanceUID); - - public StoreDatasetValidator( - IOptions featureConfiguration, - IElementMinimumValidator minimumValidator, - IQueryTagService queryTagService, - StoreMeter storeMeter, - IDicomRequestContextAccessor dicomRequestContextAccessor, - ILogger logger) - { - EnsureArg.IsNotNull(featureConfiguration?.Value, nameof(featureConfiguration)); - EnsureArg.IsNotNull(minimumValidator, nameof(minimumValidator)); - EnsureArg.IsNotNull(queryTagService, nameof(queryTagService)); - EnsureArg.IsNotNull(dicomRequestContextAccessor, nameof(dicomRequestContextAccessor)); - - _dicomRequestContextAccessor = dicomRequestContextAccessor; - _enableFullDicomItemValidation = featureConfiguration.Value.EnableFullDicomItemValidation; - _minimumValidator = minimumValidator; - _queryTagService = queryTagService; - _storeMeter = EnsureArg.IsNotNull(storeMeter, nameof(storeMeter)); - _logger = EnsureArg.IsNotNull(logger, nameof(logger)); - } - - /// - public async Task ValidateAsync( - DicomDataset dicomDataset, - string requiredStudyInstanceUid, - CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(dicomDataset, nameof(dicomDataset)); - - var validationResultBuilder = new StoreValidationResultBuilder(); - bool isV2OrAbove = EnableDropMetadata(_dicomRequestContextAccessor.RequestContext.Version); - - try - { - ValidateRequiredCoreTags(dicomDataset, requiredStudyInstanceUid); - ValidatePatientId(dicomDataset, isV2OrAbove); - } - catch (DatasetValidationException ex) when (ex.FailureCode == FailureReasonCodes.ValidationFailure) - { - validationResultBuilder.Add(ex, ex.DicomTag, isCoreTag: true); - } - - // validate input data elements - if (_enableFullDicomItemValidation) - { - ValidateAllItems(dicomDataset, validationResultBuilder); - } - else if (isV2OrAbove) - { - await ValidateAllItemsWithLeniencyAsync(dicomDataset, validationResultBuilder); - } - else - { - await ValidateIndexedItemsAsync(dicomDataset, validationResultBuilder, cancellationToken); - } - - // Validate for Implicit VR at the end - if (ImplicitValueRepresentationValidator.IsImplicitVR(dicomDataset)) - { - validationResultBuilder.Add(ValidationWarnings.DatasetDoesNotMatchSOPClass); - } - - return validationResultBuilder.Build(); - } - - private static void ValidatePatientId(DicomDataset dicomDataset, bool isV2OrAbove) - { - // Ensure required tags are present. - if (!isV2OrAbove) - { - EnsureRequiredTagIsPresentWithValue(dicomDataset, DicomTag.PatientID); - } - else - { - if (!dicomDataset.Contains(DicomTag.PatientID)) - { - throw new DatasetValidationException( - FailureReasonCodes.ValidationFailure, - string.Format( - CultureInfo.InvariantCulture, - DicomCoreResource.MissingRequiredTag, - DicomTag.PatientID), DicomTag.PatientID); - } - } - } - - private static void ValidateRequiredCoreTags(DicomDataset dicomDataset, string requiredStudyInstanceUid) - { - EnsureRequiredTagIsPresentWithValue(dicomDataset, DicomTag.SOPClassUID); - - // The format of the identifiers will be validated by fo-dicom. - string studyInstanceUid = EnsureRequiredTagIsPresentWithValue(dicomDataset, DicomTag.StudyInstanceUID); - EnsureRequiredTagIsPresentWithValue(dicomDataset, DicomTag.SeriesInstanceUID); - EnsureRequiredTagIsPresentWithValue(dicomDataset, DicomTag.SOPInstanceUID); - - // If the requestedStudyInstanceUid is specified, then the StudyInstanceUid must match, ignoring whitespace. - if (requiredStudyInstanceUid != null && - !studyInstanceUid.TrimEnd().Equals(requiredStudyInstanceUid.TrimEnd(), StringComparison.OrdinalIgnoreCase)) - { - throw new DatasetValidationException( - FailureReasonCodes.ValidationFailure, - DicomCoreResource.MismatchStudyInstanceUid, - DicomTag.StudyInstanceUID); - } - } - - private static string EnsureRequiredTagIsPresentWithValue(DicomDataset dicomDataset, DicomTag dicomTag) - { - if (dicomDataset.TryGetSingleValue(dicomTag, out string value)) - { - return value; - } - - throw new DatasetValidationException( - FailureReasonCodes.ValidationFailure, - string.Format( - CultureInfo.InvariantCulture, - DicomCoreResource.MissingRequiredTag, - dicomTag.ToString()), dicomTag); - } - - private async Task ValidateIndexedItemsAsync( - DicomDataset dicomDataset, - StoreValidationResultBuilder validationResultBuilder, - CancellationToken cancellationToken) - { - IReadOnlyCollection queryTags = await _queryTagService.GetQueryTagsAsync(cancellationToken: cancellationToken); - - foreach (QueryTag queryTag in queryTags) - { - try - { - var validationWarning = dicomDataset.ValidateQueryTag(queryTag, _minimumValidator); - - validationResultBuilder.Add(validationWarning, queryTag.Tag); - } - catch (ElementValidationException ex) - { - validationResultBuilder.Add(ex, queryTag.Tag); - _storeMeter.IndexTagValidationError.Add( - 1, - new[] - { - new KeyValuePair("ExceptionErrorCode", ex.ErrorCode.ToString()), - new KeyValuePair("ExceptionName", ex.Name), - new KeyValuePair("VR", queryTag.VR.Code) - }); - } - } - } - - private static void ValidateAllItems( - DicomDataset dicomDataset, - StoreValidationResultBuilder validationResultBuilder) - { - foreach (DicomItem item in dicomDataset) - { - try - { - item.Validate(); - } - catch (DicomValidationException ex) - { - validationResultBuilder.Add(ex, item.Tag); - } - } - } - - /// - /// Validate all items. Generate errors for core tags with leniency applied and generate warnings for all of DS items. - /// - /// Dataset to validate - /// Result builder to keep errors and warnings in as validation runs - /// - /// We only need to validate SQ and DicomElement types. The only other type under DicomItem - /// is DicomFragmentSequence, which does not implement validation and can be skipped. - /// An example of a type of DicomFragmentSequence is DicomOtherByteFragment. - /// See https://fo-dicom.github.io/stable/v5/api/FellowOakDicom.DicomItem.html - /// - private async Task ValidateAllItemsWithLeniencyAsync( - DicomDataset dicomDataset, - StoreValidationResultBuilder validationResultBuilder) - { - IReadOnlyCollection queryTags = await _queryTagService.GetQueryTagsAsync(); - - GenerateErrors(dicomDataset, validationResultBuilder, queryTags); - - GenerateValidationWarnings(dicomDataset, validationResultBuilder, queryTags); - } - - /// - /// Generate errors on result by validating each core tag in dataset - /// - /// - /// isCoreTag is utilized in store service when building a response and to know whether or not to create a failure or - /// success response. Anything added here results in a 409 conflict with errors in body. - /// - /// - /// - /// Queryable dicom tags - private void GenerateErrors(DicomDataset dicomDataset, StoreValidationResultBuilder validationResultBuilder, - IReadOnlyCollection queryTags) - { - foreach (var requiredCoreTag in RequiredCoreTags) - { - try - { - // validate with additional leniency - var validationWarning = - dicomDataset.ValidateDicomTag(requiredCoreTag, _minimumValidator, validationLevel: ValidationLevel.Default); - - validationResultBuilder.Add(validationWarning, requiredCoreTag); - } - catch (ElementValidationException ex) - { - validationResultBuilder.Add(ex, requiredCoreTag, isCoreTag: true); - _logger.LogInformation("Dicom instance validation failed with error on required core tag {Tag} with id {Id}", requiredCoreTag.DictionaryEntry.Keyword, requiredCoreTag); - _storeMeter.V2ValidationError.Add(1, - TelemetryDimension(requiredCoreTag, requiredCoreTag.GetDefaultVR(), IsIndexableTag(queryTags, requiredCoreTag))); - } - } - } - - /// - /// Generate warnings on the results by validating each item in dataset - /// - /// - /// isCoreTag is utilized in store service when building a response and to know whether or not to create a failure or - /// success response. Anything added here results in a 202 Accepted with warnings in body. - /// Since we are providing warnings as informational value, we want to use full fo-dicom validation and no leniency - /// or our minimum validators. - /// We also need to iterate through each item at a time to capture all validation issues for each and all items in - /// the dataset instead of just excepting at first issue as fo-dicom does. - /// Do not produce a warning when string invalid only due to null padding. - /// - private void GenerateValidationWarnings(DicomDataset dicomDataset, StoreValidationResultBuilder validationResultBuilder, - IReadOnlyCollection queryTags) - { - var stack = new Stack(); - stack.Push(dicomDataset); - while (stack.Count > 0) - { - DicomDataset ds = stack.Pop(); - - // add to stack to keep iterating when SQ type, otherwise validate - foreach (DicomItem item in ds) - { - if (item is DicomSequence sequence) - { - foreach (DicomDataset childDs in sequence) - { - stack.Push(childDs); - } - } - else if (item is DicomElement de) - { - try - { - if (de.ValueRepresentation.ValueType == typeof(string)) - { - string value = ds.GetString(de.Tag); - ValidateStringItemWithLeniency(value, de, queryTags); - } - else - { - de.Validate(); - } - } - catch (Exception ex) when (ex is DicomValidationException || ex is DatasetValidationException) - { - validationResultBuilder.Add(ex, item.Tag, isCoreTag: false); - if (item.Tag.IsPrivate) - { - _logger.LogInformation("Dicom instance validation succeeded but with warning on a private tag"); - } - else - { - _logger.LogInformation("Dicom instance validation succeeded but with warning on non-private tag {Tag} with id {Id}, where tag is indexable: {Indexable} and VR is {VR}", item.Tag.DictionaryEntry.Keyword, item.Tag, IsIndexableTag(queryTags, item), item.ValueRepresentation); - } - _storeMeter.V2ValidationError.Add(1, TelemetryDimension(item, IsIndexableTag(queryTags, item))); - } - } - } - } - } - - private void ValidateStringItemWithLeniency(string value, DicomElement de, IReadOnlyCollection queryTags) - { - if (de.Tag == DicomTag.PatientID && string.IsNullOrWhiteSpace(value)) - { - throw new DatasetValidationException( - FailureReasonCodes.ValidationFailure, - string.Format( - CultureInfo.InvariantCulture, - DicomCoreResource.MissingRequiredTag, - DicomTag.PatientID), DicomTag.PatientID); - } - - if (value != null && value.EndsWith('\0')) - { - ValidateWithoutNullPadding(value, de, queryTags); - } - else - { - de.Validate(); - } - } - - private static bool IsIndexableTag(IReadOnlyCollection queryTags, DicomItem de) - { - return IsIndexableTag(queryTags, de.Tag); - } - - private static bool IsIndexableTag(IReadOnlyCollection queryTags, DicomTag tag) - { - return queryTags.Any(x => x.Tag == tag); - } - - /// - /// Check if a tag is a required core tag - /// - /// tag to check if it is required - /// whether or not tag is required - public static bool IsV2CoreTag(DicomTag tag) - { - return RequiredV2CoreTags.Contains(tag); - } - - private void ValidateWithoutNullPadding(string value, DicomElement de, IReadOnlyCollection queryTags) - { - de.ValueRepresentation.ValidateString(value.TrimEnd('\0')); - _storeMeter.V2ValidationNullPaddedPassing.Add( - 1, - TelemetryDimension(de, IsIndexableTag(queryTags, de))); - } - - private static KeyValuePair[] TelemetryDimension(DicomItem item, bool isIndexableTag) => - TelemetryDimension(item.Tag, item.ValueRepresentation, isIndexableTag); - - private static KeyValuePair[] TelemetryDimension(DicomTag tag, DicomVR vr, bool isIndexableTag) => - new[] - { - new KeyValuePair("TagKeyword", tag?.DictionaryEntry?.Keyword), - new KeyValuePair("VR", vr?.ToString()), - new KeyValuePair("Tag", tag?.ToString()), - new KeyValuePair("IsIndexable", isIndexableTag.ToString()) - }; - - private static bool EnableDropMetadata(int? version) - { - return version is >= 2; - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Store/StoreErrorResult.cs b/src/Microsoft.Health.Dicom.Core/Features/Store/StoreErrorResult.cs deleted file mode 100644 index 99fd9e7336..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Store/StoreErrorResult.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Features.Store; - -public sealed class StoreErrorResult -{ - public StoreErrorResult(string error, bool isRequiredTag) - { - Error = error; - IsRequiredCoreTag = isRequiredTag; - } - - public string Error { get; } - - public bool IsRequiredCoreTag { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Store/StoreHandler.cs b/src/Microsoft.Health.Dicom.Core/Features/Store/StoreHandler.cs deleted file mode 100644 index 604155ab25..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Store/StoreHandler.cs +++ /dev/null @@ -1,69 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Globalization; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using MediatR; -using Microsoft.Health.Abstractions.Exceptions; -using Microsoft.Health.Core.Features.Security.Authorization; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Security; -using Microsoft.Health.Dicom.Core.Features.Store.Entries; -using Microsoft.Health.Dicom.Core.Messages.Store; - -namespace Microsoft.Health.Dicom.Core.Features.Store; - -public class StoreHandler : BaseHandler, IRequestHandler -{ - private readonly IDicomInstanceEntryReaderManager _dicomInstanceEntryReaderManager; - private readonly IStoreService _storeService; - - public StoreHandler( - IAuthorizationService authorizationService, - IDicomInstanceEntryReaderManager dicomInstanceEntryReaderManager, - IStoreService storeService) - : base(authorizationService) - { - _dicomInstanceEntryReaderManager = EnsureArg.IsNotNull(dicomInstanceEntryReaderManager, nameof(dicomInstanceEntryReaderManager)); - _storeService = EnsureArg.IsNotNull(storeService, nameof(storeService)); - } - - /// - public async Task Handle( - StoreRequest request, - CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(request, nameof(request)); - - if (await AuthorizationService.CheckAccess(DataActions.Write, cancellationToken) != DataActions.Write) - { - throw new UnauthorizedDicomActionException(DataActions.Write); - } - - StoreRequestValidator.ValidateRequest(request); - - // Find a reader that can parse the request body. - IDicomInstanceEntryReader dicomInstanceEntryReader = _dicomInstanceEntryReaderManager.FindReader(request.RequestContentType); - - if (dicomInstanceEntryReader == null) - { - throw new UnsupportedMediaTypeException( - string.Format(CultureInfo.InvariantCulture, DicomCoreResource.UnsupportedContentType, request.RequestContentType)); - } - - // Read list of entries. - IReadOnlyList instanceEntries = await dicomInstanceEntryReader.ReadAsync( - request.RequestContentType, - request.RequestBody, - cancellationToken); - - // Process list of entries. - return await _storeService.ProcessAsync(instanceEntries, request.StudyInstanceUid, cancellationToken); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Store/StoreOrchestrator.cs b/src/Microsoft.Health.Dicom.Core/Features/Store/StoreOrchestrator.cs deleted file mode 100644 index 4f18cb5fd1..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Store/StoreOrchestrator.cs +++ /dev/null @@ -1,254 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using FellowOakDicom; -using FellowOakDicom.Imaging; -using FellowOakDicom.IO.Buffer; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Context; -using Microsoft.Health.Dicom.Core.Features.Delete; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Features.Retrieve; -using Microsoft.Health.Dicom.Core.Features.Store.Entries; - -namespace Microsoft.Health.Dicom.Core.Features.Store; - -/// -/// Provides functionality to orchestrate the storing of the DICOM instance entry. -/// -public class StoreOrchestrator : IStoreOrchestrator -{ - private readonly IDicomRequestContextAccessor _contextAccessor; - private readonly IFileStore _fileStore; - private readonly IMetadataStore _metadataStore; - private readonly IIndexDataStore _indexDataStore; - private readonly IDeleteService _deleteService; - private readonly IQueryTagService _queryTagService; - private readonly ILogger _logger; - private readonly bool _isExternalStoreEnabled; - - public StoreOrchestrator( - IDicomRequestContextAccessor contextAccessor, - IFileStore fileStore, - IMetadataStore metadataStore, - IIndexDataStore indexDataStore, - IDeleteService deleteService, - IQueryTagService queryTagService, - IOptions featureConfiguration, - ILogger logger) - { - _contextAccessor = EnsureArg.IsNotNull(contextAccessor, nameof(contextAccessor)); - _fileStore = EnsureArg.IsNotNull(fileStore, nameof(fileStore)); - _metadataStore = EnsureArg.IsNotNull(metadataStore, nameof(metadataStore)); - _indexDataStore = EnsureArg.IsNotNull(indexDataStore, nameof(indexDataStore)); - _deleteService = EnsureArg.IsNotNull(deleteService, nameof(deleteService)); - _queryTagService = EnsureArg.IsNotNull(queryTagService, nameof(queryTagService)); - EnsureArg.IsNotNull(featureConfiguration, nameof(featureConfiguration)); - _isExternalStoreEnabled = EnsureArg.IsNotNull(featureConfiguration?.Value, nameof(featureConfiguration)).EnableExternalStore; - _logger = EnsureArg.IsNotNull(logger, nameof(logger)); - } - - /// - public async Task StoreDicomInstanceEntryAsync( - IDicomInstanceEntry dicomInstanceEntry, - CancellationToken cancellationToken - ) - { - EnsureArg.IsNotNull(dicomInstanceEntry, nameof(dicomInstanceEntry)); - - DicomDataset dicomDataset = await dicomInstanceEntry.GetDicomDatasetAsync(cancellationToken); - - var partition = _contextAccessor.RequestContext.GetPartition(); - - string dicomInstanceIdentifier = dicomDataset.ToInstanceIdentifier(partition).ToString(); - - _logger.LogInformation("Storing a DICOM instance: '{DicomInstance}'.", dicomInstanceIdentifier); - - IReadOnlyCollection queryTags = await _queryTagService.GetQueryTagsAsync(cancellationToken: cancellationToken); - long version = await _indexDataStore.BeginCreateInstanceIndexAsync(partition, dicomDataset, queryTags, cancellationToken); - var versionedInstanceIdentifier = dicomDataset.ToVersionedInstanceIdentifier(version, partition); - - try - { - // We have successfully created the index, store the files. - Task storeFileTask = StoreFileAsync(versionedInstanceIdentifier, partition.Name, dicomInstanceEntry, cancellationToken); - Task frameRangeTask = StoreFileFramesRangeAsync(dicomDataset, version, cancellationToken); - await Task.WhenAll( - storeFileTask, - StoreInstanceMetadataAsync(dicomDataset, version, cancellationToken), - frameRangeTask); - - FileProperties fileProperties = await storeFileTask; - - bool hasFrameMetadata = await frameRangeTask; - - await _indexDataStore.EndCreateInstanceIndexAsync(partition.Key, dicomDataset, version, queryTags, ShouldStoreFileProperties(fileProperties), hasFrameMetadata: hasFrameMetadata, cancellationToken: cancellationToken); - - _logger.LogInformation("Successfully stored the DICOM instance: '{DicomInstance}'.", dicomInstanceIdentifier); - - return fileProperties.ContentLength; - } - catch (Exception) - { - _logger.LogWarning("Failed to store the DICOM instance: '{DicomInstance}'.", dicomInstanceIdentifier); - - // Exception occurred while storing the file. Try delete the index. - await TryCleanupInstanceIndexAsync(versionedInstanceIdentifier); - throw; - } - } - - private FileProperties ShouldStoreFileProperties(FileProperties fileProperties) - { - return _isExternalStoreEnabled ? fileProperties : null; - } - - private async Task StoreFileAsync( - VersionedInstanceIdentifier versionedInstanceIdentifier, - string partitionName, - IDicomInstanceEntry dicomInstanceEntry, - CancellationToken cancellationToken) - { - Stream stream = await dicomInstanceEntry.GetStreamAsync(cancellationToken); - - return await _fileStore.StoreFileAsync(versionedInstanceIdentifier.Version, partitionName, stream, cancellationToken); - } - - private Task StoreInstanceMetadataAsync( - DicomDataset dicomDataset, - long version, - CancellationToken cancellationToken) - => _metadataStore.StoreInstanceMetadataAsync(dicomDataset, version, cancellationToken); - - private async Task StoreFileFramesRangeAsync( - DicomDataset dicomDataset, - long version, - CancellationToken cancellationToken) - { - bool hasFrameMetadata = false; - Dictionary framesRange = GetFramesOffset(dicomDataset); - - if (framesRange != null && framesRange.Count > 0) - { - await _metadataStore.StoreInstanceFramesRangeAsync(version, framesRange, cancellationToken); - hasFrameMetadata = true; - } - return hasFrameMetadata; - } - - [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Method will not throw.")] - private async Task TryCleanupInstanceIndexAsync(VersionedInstanceIdentifier versionedInstanceIdentifier) - { - try - { - // In case the request is canceled and one of the operation failed, we still want to cleanup. - // Therefore, we will not be using the same cancellation token as the request itself. - await _deleteService.DeleteInstanceNowAsync( - versionedInstanceIdentifier.StudyInstanceUid, - versionedInstanceIdentifier.SeriesInstanceUid, - versionedInstanceIdentifier.SopInstanceUid, - CancellationToken.None); - } - catch (Exception) - { - // Fire and forget. - } - } - - internal static Dictionary GetFramesOffset(DicomDataset dataset) - { - if (!dataset.TryGetPixelData(out DicomPixelData dicomPixel)) - { - return null; - } - - if (dicomPixel.NumberOfFrames < 1) - { - return null; - } - - var pixelData = dataset.GetDicomItem(DicomTag.PixelData); - var framesRange = new Dictionary(); - - // todo support case where fragments != frames. - // This means offsettable matches the frames and we have to parse the bytes in pixelData to find the right fragment and end at the right fragment. - // there is also a 8 byte tag inbetween the fragment data that we need to handlee. - // fo-dicom/DicomPixelData.cs/GetFrame has the logic - if (pixelData is DicomFragmentSequence pixelDataFragment - && pixelDataFragment.Fragments.Count == dicomPixel.NumberOfFrames) - { - for (int i = 0; i < pixelDataFragment.Fragments.Count; i++) - { - var fragment = pixelDataFragment.Fragments[i]; - if (TryGetBufferPosition(fragment, out long position, out long size)) - { - framesRange.Add(i, new FrameRange(position, size)); - } - } - } - else if (pixelData is DicomOtherByte) - { - var dicomPixelOtherByte = dataset.GetDicomItem(DicomTag.PixelData); - - for (int i = 0; i < dicomPixel.NumberOfFrames; i++) - { - IByteBuffer byteBuffer = dicomPixel.GetFrame(i); - if (TryGetBufferPosition(dicomPixelOtherByte.Buffer, out long position, out long size) - && byteBuffer is RangeByteBuffer rangeByteBuffer) - { - framesRange.Add(i, new FrameRange(position + rangeByteBuffer.Offset, rangeByteBuffer.Length)); - } - } - } - else if (pixelData is DicomOtherWord) - { - var dicomPixelWordByte = dataset.GetDicomItem(DicomTag.PixelData); - - for (int i = 0; i < dicomPixel.NumberOfFrames; i++) - { - IByteBuffer byteBuffer = dicomPixel.GetFrame(i); - if (TryGetBufferPosition(dicomPixelWordByte.Buffer, out long position, out long size) - && byteBuffer is RangeByteBuffer rangeByteBuffer) - { - framesRange.Add(i, new FrameRange(position + rangeByteBuffer.Offset, rangeByteBuffer.Length)); - } - } - } - - return framesRange.Count > 0 ? framesRange : null; - } - - private static bool TryGetBufferPosition(IByteBuffer buffer, out long position, out long size) - { - bool result = false; - position = 0; - size = 0; - if (buffer is StreamByteBuffer streamByteBuffer) - { - position = streamByteBuffer.Position; - size = streamByteBuffer.Size; - result = true; - } - else if (buffer is FileByteBuffer fileByteBuffer) - { - position = fileByteBuffer.Position; - size = fileByteBuffer.Size; - result = true; - } - return result; - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Store/StoreRequestValidator.cs b/src/Microsoft.Health.Dicom.Core/Features/Store/StoreRequestValidator.cs deleted file mode 100644 index 852ca9e62b..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Store/StoreRequestValidator.cs +++ /dev/null @@ -1,35 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Validation; -using Microsoft.Health.Dicom.Core.Messages.Store; - -namespace Microsoft.Health.Dicom.Core.Features.Store; - -/// -/// Provides functionality to validate an . -/// -public static class StoreRequestValidator -{ - /// - /// Validates an . - /// - /// The request to validate. - /// Thrown when request body is missing. - /// Thrown when the specified StudyInstanceUID is not a valid identifier. - // TODO cleanup this method with Unit tests #72595 - public static void ValidateRequest(StoreRequest request) - { - EnsureArg.IsNotNull(request, nameof(request)); - if (request.RequestBody == null) - { - throw new BadRequestException(DicomCoreResource.MissingRequestBody); - } - - UidValidation.Validate(request.StudyInstanceUid, nameof(request.StudyInstanceUid), allowEmpty: true); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Store/StoreResponseBuilder.cs b/src/Microsoft.Health.Dicom.Core/Features/Store/StoreResponseBuilder.cs deleted file mode 100644 index b29e459db8..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Store/StoreResponseBuilder.cs +++ /dev/null @@ -1,200 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Linq; -using EnsureThat; -using FellowOakDicom; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Core.Features.Routing; -using Microsoft.Health.Dicom.Core.Messages.Store; - -namespace Microsoft.Health.Dicom.Core.Features.Store; - -/// -/// Provides functionality to build the response for the store transaction. -/// -public class StoreResponseBuilder : IStoreResponseBuilder -{ - private readonly IUrlResolver _urlResolver; - - private DicomDataset _dataset; - - private string _message; - - private readonly bool _isPartitionEnabled; - - public StoreResponseBuilder( - IUrlResolver urlResolver, - IOptions featureConfiguration - ) - { - EnsureArg.IsNotNull(urlResolver, nameof(urlResolver)); - EnsureArg.IsNotNull(featureConfiguration, nameof(featureConfiguration)); - - _urlResolver = urlResolver; - _isPartitionEnabled = featureConfiguration.Value.EnableDataPartitions; - } - - /// - public StoreResponse BuildResponse(string studyInstanceUid, bool returnWarning202 = false) - { - bool hasSuccess = _dataset?.TryGetSequence(DicomTag.ReferencedSOPSequence, out _) ?? false; - bool hasFailure = _dataset?.TryGetSequence(DicomTag.FailedSOPSequence, out _) ?? false; - - StoreResponseStatus status = StoreResponseStatus.None; - - if (hasSuccess && hasFailure) - { - // There are both successes and failures. - status = StoreResponseStatus.PartialSuccess; - } - else if (hasSuccess) - { - if (returnWarning202 && HasWarningReasonCode()) - { - // if we have warning reason code on any of the instances, status code should reflect that - status = StoreResponseStatus.PartialSuccess; - } - else - { - // There are only success. - status = StoreResponseStatus.Success; - } - } - else if (hasFailure) - { - // There are only failures. - status = StoreResponseStatus.Failure; - } - - if (hasSuccess && studyInstanceUid != null) - { - _dataset.Add(DicomTag.RetrieveURL, _urlResolver.ResolveRetrieveStudyUri(studyInstanceUid).ToString()); - } - - return new StoreResponse(status, _dataset, _message); - } - - private bool HasWarningReasonCode() - { - DicomSequence referencedSOPSequence = _dataset.GetSequence(DicomTag.ReferencedSOPSequence); - return referencedSOPSequence - .Any(ds => ds.TryGetString(DicomTag.WarningReason, out _) == true); - } - - /// - public void AddSuccess(DicomDataset dicomDataset, - StoreValidationResult storeValidationResult, - Partition partition, - ushort? warningReasonCode = null, - bool buildWarningSequence = false) - { - EnsureArg.IsNotNull(dicomDataset, nameof(dicomDataset)); - EnsureArg.IsNotNull(storeValidationResult, nameof(storeValidationResult)); - - CreateDatasetIfNeeded(); - - if (!_dataset.TryGetSequence(DicomTag.ReferencedSOPSequence, out DicomSequence referencedSopSequence)) - { - referencedSopSequence = new DicomSequence(DicomTag.ReferencedSOPSequence); - - _dataset.Add(referencedSopSequence); - } - - var dicomInstance = dicomDataset.ToInstanceIdentifier(partition); - - var referencedSop = new DicomDataset - { - { DicomTag.ReferencedSOPInstanceUID, dicomDataset.GetSingleValue(DicomTag.SOPInstanceUID) }, - { DicomTag.RetrieveURL, _urlResolver.ResolveRetrieveInstanceUri(dicomInstance, _isPartitionEnabled).ToString() }, - { DicomTag.ReferencedSOPClassUID, dicomDataset.GetFirstValueOrDefault(DicomTag.SOPClassUID) }, - }; - - if (warningReasonCode.HasValue) - { - referencedSop.Add(DicomTag.WarningReason, warningReasonCode.Value); - } - - if (buildWarningSequence) - { - // add comment Sq / list of warnings here - var warnings = storeValidationResult.InvalidTagErrors.Values.Select( - error => new DicomDataset( - new DicomLongString( - DicomTag.ErrorComment, - error.Error))) - .ToArray(); - - var failedSequence = new DicomSequence( - DicomTag.FailedAttributesSequence, - warnings); - referencedSop.Add(failedSequence); - } - - referencedSopSequence.Items.Add(referencedSop); - } - - /// - public void AddFailure(DicomDataset dicomDataset, ushort failureReasonCode, StoreValidationResult storeValidationResult = null) - { - CreateDatasetIfNeeded(); - - if (!_dataset.TryGetSequence(DicomTag.FailedSOPSequence, out DicomSequence failedSopSequence)) - { - failedSopSequence = new DicomSequence(DicomTag.FailedSOPSequence); - - _dataset.Add(failedSopSequence); - } - - var failedSop = new DicomDataset() - { - { DicomTag.FailureReason, failureReasonCode }, - }; - - // We want to turn off auto validation for FailedSOPSequence item - // because the failure might be caused by invalid UID value. -#pragma warning disable CS0618 // Type or member is obsolete - failedSop.AutoValidate = false; -#pragma warning restore CS0618 // Type or member is obsolete - - failedSop.AddValueIfNotNull( - DicomTag.ReferencedSOPInstanceUID, - dicomDataset?.GetFirstValueOrDefault(DicomTag.SOPInstanceUID)); - - failedSop.AddValueIfNotNull( - DicomTag.ReferencedSOPClassUID, - dicomDataset?.GetFirstValueOrDefault(DicomTag.SOPClassUID)); - - if (storeValidationResult != null) - { - var failedAttributes = storeValidationResult.InvalidTagErrors.Values.Select( - error => new DicomDataset( - new DicomLongString( - DicomTag.ErrorComment, - error.Error))).ToArray(); - - var failedAttributeSequence = new DicomSequence(DicomTag.FailedAttributesSequence, failedAttributes); - failedSop.Add(failedAttributeSequence); - } - - failedSopSequence.Items.Add(failedSop); - } - - private void CreateDatasetIfNeeded() - { - if (_dataset == null) - { - _dataset = new DicomDataset(); - } - } - - public void SetWarningMessage(string message) - { - _message = message; - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Store/StoreService.cs b/src/Microsoft.Health.Dicom.Core/Features/Store/StoreService.cs deleted file mode 100644 index b58a97c58a..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Store/StoreService.cs +++ /dev/null @@ -1,309 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using FellowOakDicom; -using Microsoft.ApplicationInsights; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Context; -using Microsoft.Health.Dicom.Core.Features.Diagnostic; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Core.Features.Store.Entries; -using Microsoft.Health.Dicom.Core.Features.Telemetry; -using Microsoft.Health.Dicom.Core.Messages.Store; - -namespace Microsoft.Health.Dicom.Core.Features.Store; - -/// -/// Provides functionality to process the list of . -/// -public class StoreService : IStoreService -{ - private static readonly Action LogValidationFailedDelegate = - LoggerMessage.Define( - LogLevel.Information, - default, - "Validation failed for the DICOM instance entry at index '{DicomInstanceEntryIndex}'. Failure code: {FailureCode}."); - - private static readonly Action LogValidationSucceededWithWarningDelegate = - LoggerMessage.Define( - LogLevel.Warning, - default, - "Validation succeeded with warning(s) for the DICOM instance entry at index '{DicomInstanceEntryIndex}'. {WarningCode}"); - - private static readonly Action LogFailedToStoreDelegate = - LoggerMessage.Define( - LogLevel.Warning, - default, - "Failed to store the DICOM instance entry at index '{DicomInstanceEntryIndex}'. Failure code: {FailureCode}."); - - private static readonly Action LogSuccessfullyStoredDelegate = - LoggerMessage.Define( - LogLevel.Information, - default, - "Successfully stored the DICOM instance entry at index '{DicomInstanceEntryIndex}'."); - - private static readonly Action LogFailedToDisposeDelegate = - LoggerMessage.Define( - LogLevel.Warning, - default, - "Failed to dispose the DICOM instance entry at index '{DicomInstanceEntryIndex}'."); - - private readonly IStoreResponseBuilder _storeResponseBuilder; - private readonly IStoreDatasetValidator _dicomDatasetValidator; - private readonly IStoreOrchestrator _storeOrchestrator; - private readonly IDicomRequestContextAccessor _dicomRequestContextAccessor; - private readonly IDicomTelemetryClient _dicomTelemetryClient; - private readonly TelemetryClient _telemetryClient; - private readonly StoreMeter _storeMeter; - private readonly ILogger _logger; - - private IReadOnlyList _dicomInstanceEntries; - private string _requiredStudyInstanceUid; - - public StoreService( - IStoreResponseBuilder storeResponseBuilder, - IStoreDatasetValidator dicomDatasetValidator, - IStoreOrchestrator storeOrchestrator, - IDicomRequestContextAccessor dicomRequestContextAccessor, - StoreMeter storeMeter, - ILogger logger, - IOptions featureConfiguration, - IDicomTelemetryClient dicomTelemetryClient, - TelemetryClient telemetryClient) - { - EnsureArg.IsNotNull(featureConfiguration?.Value, nameof(featureConfiguration)); - _storeResponseBuilder = EnsureArg.IsNotNull(storeResponseBuilder, nameof(storeResponseBuilder)); - _dicomDatasetValidator = EnsureArg.IsNotNull(dicomDatasetValidator, nameof(dicomDatasetValidator)); - _storeOrchestrator = EnsureArg.IsNotNull(storeOrchestrator, nameof(storeOrchestrator)); - _dicomRequestContextAccessor = EnsureArg.IsNotNull(dicomRequestContextAccessor, nameof(dicomRequestContextAccessor)); - _dicomTelemetryClient = EnsureArg.IsNotNull(dicomTelemetryClient, nameof(dicomTelemetryClient)); - _telemetryClient = EnsureArg.IsNotNull(telemetryClient, nameof(_telemetryClient)); - _storeMeter = EnsureArg.IsNotNull(storeMeter, nameof(storeMeter)); - _logger = EnsureArg.IsNotNull(logger, nameof(logger)); - } - - /// - public async Task ProcessAsync( - IReadOnlyList instanceEntries, - string requiredStudyInstanceUid, - CancellationToken cancellationToken) - { - bool returnWarning202 = _dicomRequestContextAccessor.RequestContext.Version is >= 2; - if (instanceEntries != null) - { - _dicomRequestContextAccessor.RequestContext.PartCount = instanceEntries.Count; - _dicomInstanceEntries = instanceEntries; - _requiredStudyInstanceUid = requiredStudyInstanceUid; - _dicomTelemetryClient.TrackInstanceCount(instanceEntries.Count); - - long totalLength = 0, minLength = 0, maxLength = 0; - - for (int index = 0; index < instanceEntries.Count; index++) - { - try - { - long? length = await ProcessDicomInstanceEntryAsync(index, cancellationToken); - if (length != null) - { - long len = length.GetValueOrDefault(); - totalLength += len; - minLength = Math.Min(minLength, len); - maxLength = Math.Max(maxLength, len); - // Update Telemetry - _storeMeter.InstanceLength.Record(len); - _dicomRequestContextAccessor.RequestContext.TotalDicomEgressToStorageBytes += len; - } - } - finally - { - // Update Requests Telemetry - _dicomTelemetryClient.TrackTotalInstanceBytes(totalLength); - _dicomTelemetryClient.TrackMinInstanceBytes(minLength); - _dicomTelemetryClient.TrackMaxInstanceBytes(maxLength); - - // Fire and forget. - int capturedIndex = index; - - _ = Task.Run(() => DisposeResourceAsync(capturedIndex), CancellationToken.None); - } - } - } - - return _storeResponseBuilder.BuildResponse(requiredStudyInstanceUid, returnWarning202); - } - - [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Will reevaluate exceptions when refactoring validation.")] - private async Task ProcessDicomInstanceEntryAsync(int index, CancellationToken cancellationToken) - { - IDicomInstanceEntry dicomInstanceEntry = _dicomInstanceEntries[index]; - - ushort? warningReasonCode = null; - DicomDataset dicomDataset = null; - StoreValidationResult storeValidatorResult = null; - - bool dropMetadata = _dicomRequestContextAccessor.RequestContext.Version is >= 2; - Partition partition = _dicomRequestContextAccessor.RequestContext.DataPartition; - - try - { - // Open and validate the DICOM instance. - dicomDataset = await dicomInstanceEntry.GetDicomDatasetAsync(cancellationToken); - - storeValidatorResult = await _dicomDatasetValidator.ValidateAsync(dicomDataset, _requiredStudyInstanceUid, cancellationToken); - - // We have different ways to handle with warnings. - // DatasetDoesNotMatchSOPClass is defined in Dicom Standards (https://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_I.2.html), put into Warning Reason dicom tag - if ((storeValidatorResult.WarningCodes & ValidationWarnings.DatasetDoesNotMatchSOPClass) == ValidationWarnings.DatasetDoesNotMatchSOPClass) - { - warningReasonCode = WarningReasonCodes.DatasetDoesNotMatchSOPClass; - - LogValidationSucceededWithWarningDelegate(_logger, index, WarningReasonCodes.DatasetDoesNotMatchSOPClass, null); - } - - // IndexedDicomTagHasMultipleValues is our warning, put into http Warning header. - if ((storeValidatorResult.WarningCodes & ValidationWarnings.IndexedDicomTagHasMultipleValues) == ValidationWarnings.IndexedDicomTagHasMultipleValues) - { - _storeResponseBuilder.SetWarningMessage(DicomCoreResource.IndexedDicomTagHasMultipleValues); - } - - if (dropMetadata) - { - // if any core tag errors occured, log as failure and return. otherwise we drop the invalid tag - if (storeValidatorResult.HasCoreTagError) - { - LogFailure(index, dicomDataset, storeValidatorResult); - return null; - } - - DropInvalidMetadata(storeValidatorResult, dicomDataset, partition); - - // set warning code if none set yet when there were validation warnings - if (storeValidatorResult.InvalidTagErrors.Any()) - { - warningReasonCode ??= WarningReasonCodes.DatasetHasValidationWarnings; - } - } - else - { - // if any tag errors occured, log as failure and return - if (storeValidatorResult.InvalidTagErrors.Any()) - { - LogFailure(index, dicomDataset, storeValidatorResult); - return null; - } - } - } - catch (OperationCanceledException) - { - throw; - } - catch (Exception ex) - { - ushort failureCode = ex switch - { - DatasetValidationException dve => dve.FailureCode, - DicomValidationException or ValidationException => FailureReasonCodes.ValidationFailure, - _ => FailureReasonCodes.ProcessingFailure, - }; - - LogValidationFailedDelegate(_logger, index, failureCode, ex); - - _storeResponseBuilder.AddFailure(dicomDataset, failureCode); - return null; - } - - try - { - // Store the instance. - long length = await _storeOrchestrator.StoreDicomInstanceEntryAsync( - dicomInstanceEntry, - cancellationToken); - - LogSuccessfullyStoredDelegate(_logger, index, null); - - _storeResponseBuilder.AddSuccess( - dicomDataset, - storeValidatorResult, - partition, - warningReasonCode, - buildWarningSequence: dropMetadata - ); - return length; - } - catch (ConditionalExternalException cee) when (cee.IsExternal) - { - throw; - } - catch (DataStoreException dse) when (dse.InnerException is OperationCanceledException) - { - throw; - } - catch (Exception ex) - { - ushort failureCode = ex switch - { - PendingInstanceException => FailureReasonCodes.PendingSopInstance, - InstanceAlreadyExistsException => FailureReasonCodes.SopInstanceAlreadyExists, - _ => FailureReasonCodes.ProcessingFailure, - }; - - LogFailedToStoreDelegate(_logger, index, failureCode, ex); - - _storeResponseBuilder.AddFailure(dicomDataset, failureCode); - return null; - } - } - - private void LogFailure(int index, DicomDataset dicomDataset, StoreValidationResult storeValidatorResult) - { - ushort failureCode = FailureReasonCodes.ValidationFailure; - LogValidationFailedDelegate(_logger, index, failureCode, null); - _storeResponseBuilder.AddFailure(dicomDataset, failureCode, storeValidatorResult); - } - - private void DropInvalidMetadata(StoreValidationResult storeValidatorResult, DicomDataset dicomDataset, Partition partition) - { - var identifier = dicomDataset.ToInstanceIdentifier(partition); - foreach ((DicomTag tag, StoreErrorResult result) in storeValidatorResult.InvalidTagErrors) - { - if (!StoreDatasetValidator.IsV2CoreTag(tag)) - { - // drop invalid metadata if not a core tag - dicomDataset.Remove(tag); - - string message = result.Error; - _telemetryClient.ForwardLogTrace( - $"{message}. This attribute will not be present when retrieving study, series, or instance metadata resources, nor can it be used in searches." + - " However, it will still be present when retrieving study, series, or instance resources.", - identifier, - ApplicationInsights.DataContracts.SeverityLevel.Warning); - } - } - } - - [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Ignore errors during disposal.")] - private async Task DisposeResourceAsync(int index) - { - try - { - await _dicomInstanceEntries[index].DisposeAsync(); - } - catch (Exception ex) - { - LogFailedToDisposeDelegate(_logger, index, ex); - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Store/StoreValidationResult.cs b/src/Microsoft.Health.Dicom.Core/Features/Store/StoreValidationResult.cs deleted file mode 100644 index f0feceaee9..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Store/StoreValidationResult.cs +++ /dev/null @@ -1,31 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Linq; -using FellowOakDicom; - -namespace Microsoft.Health.Dicom.Core.Features.Store; - -public sealed class StoreValidationResult -{ - public StoreValidationResult( - IReadOnlyCollection warnings, - ValidationWarnings validationWarnings, - IReadOnlyDictionary invalidTagErrors) - { - Warnings = warnings; - WarningCodes = validationWarnings; - InvalidTagErrors = invalidTagErrors; - } - - public IReadOnlyCollection Warnings { get; } - - public ValidationWarnings WarningCodes { get; } - - public IReadOnlyDictionary InvalidTagErrors { get; } - - public bool HasCoreTagError => InvalidTagErrors.Any(invalidTagError => invalidTagError.Value.IsRequiredCoreTag); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Store/StoreValidationResultBuilder.cs b/src/Microsoft.Health.Dicom.Core/Features/Store/StoreValidationResultBuilder.cs deleted file mode 100644 index 4acd0277e3..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Store/StoreValidationResultBuilder.cs +++ /dev/null @@ -1,80 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Globalization; -using EnsureThat; -using FellowOakDicom; - -namespace Microsoft.Health.Dicom.Core.Features.Store; - -internal sealed class StoreValidationResultBuilder -{ - private readonly List _warningMessages; - private readonly Dictionary _invalidDicomTagErrors; - - // TODO: Remove this during the cleanup. (this is to support the existing validator behavior) - private ValidationWarnings _warningCodes; - - public StoreValidationResultBuilder() - { - _warningMessages = new List(); - _invalidDicomTagErrors = new Dictionary(); - - // TODO: Remove these during the cleanup. (this is to support the existing validator behavior) - _warningCodes = ValidationWarnings.None; - } - - public StoreValidationResult Build() - { - return new StoreValidationResult( - _warningMessages, - _warningCodes, - _invalidDicomTagErrors); - } - - public void Add(Exception ex, DicomTag dicomTag, bool isCoreTag = false) - { - var errorResult = new StoreErrorResult(GetFormattedText(ex?.Message, dicomTag), isCoreTag); - _invalidDicomTagErrors.TryAdd(dicomTag, errorResult); - } - - public void Add(ValidationWarnings warningCode, DicomTag dicomTag = null) - { - // TODO: Remove this during the cleanup. (this is to support the existing validator behavior) - _warningCodes |= warningCode; - - if (warningCode != ValidationWarnings.None) - { - _warningMessages.Add(GetFormattedText(GetWarningMessage(warningCode), dicomTag)); - } - } - - private static string GetFormattedText(string message, DicomTag dicomTag = null) - { - EnsureArg.IsNotNull(message, nameof(message)); - - if (dicomTag == null) - return message; - - return string.Format( - CultureInfo.InvariantCulture, - DicomCoreResource.ErrorMessageFormat, - ErrorNumbers.ValidationFailure, - dicomTag.ToString(), - message); - } - - private static string GetWarningMessage(ValidationWarnings warningCode) - { - return warningCode switch - { - ValidationWarnings.IndexedDicomTagHasMultipleValues => DicomCoreResource.ErrorMessageMultiValues, - ValidationWarnings.DatasetDoesNotMatchSOPClass => DicomCoreResource.DatasetDoesNotMatchSOPClass, - _ => string.Empty, - }; - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Store/ValidationWarnings.cs b/src/Microsoft.Health.Dicom.Core/Features/Store/ValidationWarnings.cs deleted file mode 100644 index f2e7e3a856..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Store/ValidationWarnings.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; - -namespace Microsoft.Health.Dicom.Core.Features.Store; - -/// -/// Represent warnings when validation. -/// -[Flags] -public enum ValidationWarnings -{ - /// - /// No warnings. - /// - None = 0, - - /// - /// One or more Dicom tags in DicomDataset has multiple values - /// - IndexedDicomTagHasMultipleValues = 1, - - /// - /// Data Set does not match SOP Class. - /// - DatasetDoesNotMatchSOPClass = 2, -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Store/WarningReasonCodes.cs b/src/Microsoft.Health.Dicom.Core/Features/Store/WarningReasonCodes.cs deleted file mode 100644 index cf5459e4ed..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Store/WarningReasonCodes.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Features.Store; - -/// -/// If any of the warning codes are modified, please check they match the DICOM conformance statement. -/// -internal static class WarningReasonCodes -{ - - /// - /// Data Set does not match SOP Class - /// - /// - /// The Studies Store Transaction (Section 10.5) observed that the Data Set did not match the constraints of the SOP Class during storage of the instance. - /// - public const ushort DatasetDoesNotMatchSOPClass = 45063; - - /// - /// Data Set has validation warnings - /// - /// - /// The Studies Store Transaction (Section 10.5) observed that the Data Set has validation warnings. - /// - public const ushort DatasetHasValidationWarnings = 1; - -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Telemetry/DeleteMeter.cs b/src/Microsoft.Health.Dicom.Core/Features/Telemetry/DeleteMeter.cs deleted file mode 100644 index 65296d3072..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Telemetry/DeleteMeter.cs +++ /dev/null @@ -1,32 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Diagnostics.Metrics; - -namespace Microsoft.Health.Dicom.Core.Features.Telemetry; - -public sealed class DeleteMeter : IDisposable -{ - private readonly Meter _meter; - - public DeleteMeter() - : this($"{OpenTelemetryLabels.BaseMeterName}.Delete", "1.0") - { } - - internal DeleteMeter(string name, string version) - { - _meter = new Meter(name, version); - OldestRequestedDeletion = _meter.CreateCounter(nameof(OldestRequestedDeletion), "seconds", "Oldest instance waiting to be deleted in seconds"); - CountDeletionsMaxRetry = _meter.CreateCounter(nameof(CountDeletionsMaxRetry), description: "Number of exhausted instance deletion attempts"); - } - - public Counter OldestRequestedDeletion { get; } - - public Counter CountDeletionsMaxRetry { get; } - - public void Dispose() - => _meter.Dispose(); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Telemetry/IDicomTelemetryClient.cs b/src/Microsoft.Health.Dicom.Core/Features/Telemetry/IDicomTelemetryClient.cs deleted file mode 100644 index 463cb95890..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Telemetry/IDicomTelemetryClient.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - - -namespace Microsoft.Health.Dicom.Core.Features.Telemetry; - -public interface IDicomTelemetryClient -{ - void TrackMetric(string name, int value); - - void TrackMetric(string name, long value); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Telemetry/MetricPointExtensions.cs b/src/Microsoft.Health.Dicom.Core/Features/Telemetry/MetricPointExtensions.cs deleted file mode 100644 index e91e76b0eb..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Telemetry/MetricPointExtensions.cs +++ /dev/null @@ -1,51 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using OpenTelemetry.Metrics; - -namespace Microsoft.Health.Dicom.Core.Features.Telemetry; - -/// -/// Since only enumerators are exposed publicly for working with tags or getting the collection of -/// metrics, these extension facilitate getting both. -/// -public static class MetricPointExtensions -{ - /// - /// Get tags key value pairs from metric point. - /// - public static Dictionary GetTags(this MetricPoint metricPoint) - { - var tags = new Dictionary(); - foreach (var pair in metricPoint.Tags) - { - tags.Add(pair.Key, pair.Value); - } - - return tags; - } - - /// - /// Get all metrics emitted after flushing. - /// - [SuppressMessage("Performance", "CA1859: Use concrete types when possible for improved performance", Justification = "Result should be read-only.")] - public static IReadOnlyList GetMetricPoints(this ICollection exportedItems, string metricName) - { - MetricPointsAccessor accessor = exportedItems - .Single(item => item.Name.Equals(metricName, StringComparison.Ordinal)) - .GetMetricPoints(); - var metrics = new List(); - foreach (MetricPoint metricPoint in accessor) - { - metrics.Add(metricPoint); - } - - return metrics; - } -} \ No newline at end of file diff --git a/src/Microsoft.Health.Dicom.Core/Features/Telemetry/OpenTelemetryLabels.cs b/src/Microsoft.Health.Dicom.Core/Features/Telemetry/OpenTelemetryLabels.cs deleted file mode 100644 index e66d2dd4b7..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Telemetry/OpenTelemetryLabels.cs +++ /dev/null @@ -1,11 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Features.Telemetry; - -public static class OpenTelemetryLabels -{ - public const string BaseMeterName = "Microsoft.Health.Dicom"; -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Telemetry/RetrieveMeter.cs b/src/Microsoft.Health.Dicom.Core/Features/Telemetry/RetrieveMeter.cs deleted file mode 100644 index dbe2e2d862..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Telemetry/RetrieveMeter.cs +++ /dev/null @@ -1,37 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Diagnostics.Metrics; - -namespace Microsoft.Health.Dicom.Core.Features.Telemetry; - -public sealed class RetrieveMeter : IDisposable -{ - private readonly Meter _meter; - - public RetrieveMeter() - { - _meter = new Meter($"{OpenTelemetryLabels.BaseMeterName}.Retrieve", "1.0"); - RetrieveInstanceMetadataCount = _meter.CreateCounter(nameof(RetrieveInstanceMetadataCount), description: "Count of metadata retrieved"); - RetrieveInstanceCount = _meter.CreateCounter(nameof(RetrieveInstanceCount), description: "Count of instances retrieved"); - } - - public Counter RetrieveInstanceMetadataCount { get; } - - public Counter RetrieveInstanceCount { get; } - - public static KeyValuePair[] RetrieveInstanceCountTelemetryDimension(bool isTranscoding = false, bool hasFrameMetadata = false, bool isRendered = false) => - new[] - { - new KeyValuePair("IsTranscoding", isTranscoding), - new KeyValuePair("IsRendered", isRendered), - new KeyValuePair("HasFrameMetadata", hasFrameMetadata), - }; - - public void Dispose() - => _meter.Dispose(); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Telemetry/StoreMeter.cs b/src/Microsoft.Health.Dicom.Core/Features/Telemetry/StoreMeter.cs deleted file mode 100644 index a1f9777f30..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Telemetry/StoreMeter.cs +++ /dev/null @@ -1,33 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Diagnostics.Metrics; - -namespace Microsoft.Health.Dicom.Core.Features.Telemetry; - -public sealed class StoreMeter : IDisposable -{ - private readonly Meter _meter; - - public StoreMeter() - { - _meter = new Meter($"{OpenTelemetryLabels.BaseMeterName}.Store", "1.0"); - IndexTagValidationError = _meter.CreateCounter(nameof(IndexTagValidationError), description: "Count of index tag validation errors"); - InstanceLength = _meter.CreateHistogram(nameof(InstanceLength), "bytes", "Length of the instance"); - V2ValidationError = _meter.CreateCounter(nameof(V2ValidationError), "Count of validation errors when validating all items with leniency"); - V2ValidationNullPaddedPassing = _meter.CreateCounter(nameof(V2ValidationNullPaddedPassing), "Count of null padded values passing leniency"); - } - - public Counter IndexTagValidationError { get; } - - public Counter V2ValidationError { get; } - public Counter V2ValidationNullPaddedPassing { get; } - - public Histogram InstanceLength { get; } - - public void Dispose() - => _meter.Dispose(); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Telemetry/UpdateMeter.cs b/src/Microsoft.Health.Dicom.Core/Features/Telemetry/UpdateMeter.cs deleted file mode 100644 index e3351d2d8c..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Telemetry/UpdateMeter.cs +++ /dev/null @@ -1,25 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Diagnostics.Metrics; - -namespace Microsoft.Health.Dicom.Core.Features.Telemetry; - -public sealed class UpdateMeter : IDisposable -{ - private readonly Meter _meter; - - public UpdateMeter() - { - _meter = new Meter($"{OpenTelemetryLabels.BaseMeterName}.Update", "1.0"); - UpdatedInstances = _meter.CreateCounter(nameof(UpdatedInstances), description: "Count of instances updated successfully"); - } - - public Counter UpdatedInstances { get; } - - public void Dispose() - => _meter.Dispose(); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Update/IUpdateInstanceOperationService.cs b/src/Microsoft.Health.Dicom.Core/Features/Update/IUpdateInstanceOperationService.cs deleted file mode 100644 index 86c2e666ca..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Update/IUpdateInstanceOperationService.cs +++ /dev/null @@ -1,28 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Health.Dicom.Core.Messages.Update; -using Microsoft.Health.Dicom.Core.Models.Update; - -namespace Microsoft.Health.Dicom.Core.Features.Update; - -/// -/// Provides functionality to queue the operation for updating the study attributes -/// for a list of studyInstanceUids. -/// -public interface IUpdateInstanceOperationService -{ - /// - /// Queues the operation for updating the . - /// - /// Update spec that has the studyInstanceUids and DicomDataset - /// The cancellation token. - /// A task that represents the asynchronous process operation. - public Task QueueUpdateOperationAsync( - UpdateSpecification updateSpecification, - CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Update/IUpdateInstanceService.cs b/src/Microsoft.Health.Dicom.Core/Features/Update/IUpdateInstanceService.cs deleted file mode 100644 index c6cc75c130..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Update/IUpdateInstanceService.cs +++ /dev/null @@ -1,40 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Threading; -using System.Threading.Tasks; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Features.Partitioning; - -namespace Microsoft.Health.Dicom.Core.Features.Update; - -public interface IUpdateInstanceService -{ - /// - /// Asynchronously update instance blobs - /// - /// Instance to update - /// Dataset to update - /// Partition to update data within - /// Cancellation token - /// A task that represents the asynchronous UpdateInstanceBlobAsync operation - /// - /// is . - /// - public Task UpdateInstanceBlobAsync(InstanceMetadata instance, DicomDataset datasetToUpdate, Partition partition, CancellationToken cancellationToken = default); - - /// - /// Asynchronously deletes old blob - /// - /// Unique file identifier, watermark - /// Partition - /// File properties of instance to be deleted - /// Cancellation token - /// A task that represents the asynchronous DeleteInstanceBlobAsync operation - public Task DeleteInstanceBlobAsync(long fileIdentifier, Partition partition, FileProperties fileProperties, CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Update/UpdateInstanceHandler.cs b/src/Microsoft.Health.Dicom.Core/Features/Update/UpdateInstanceHandler.cs deleted file mode 100644 index fd816dd731..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Update/UpdateInstanceHandler.cs +++ /dev/null @@ -1,36 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using MediatR; -using Microsoft.Health.Core.Features.Security.Authorization; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Security; -using Microsoft.Health.Dicom.Core.Messages.Update; - -namespace Microsoft.Health.Dicom.Core.Features.Update; - -internal class UpdateInstanceHandler : BaseHandler, IRequestHandler -{ - private readonly IUpdateInstanceOperationService _updateInstanceOperationService; - - public UpdateInstanceHandler(IAuthorizationService authorizationService, IUpdateInstanceOperationService updateInstanceOperationService) - : base(authorizationService) - => _updateInstanceOperationService = EnsureArg.IsNotNull(updateInstanceOperationService, nameof(updateInstanceOperationService)); - - public async Task Handle(UpdateInstanceRequest request, CancellationToken cancellationToken = default) - { - EnsureArg.IsNotNull(request, nameof(request)); - EnsureArg.IsNotNull(request.UpdateSpec, nameof(request.UpdateSpec)); - - if (await AuthorizationService.CheckAccess(DataActions.Write, cancellationToken) != DataActions.Write) - throw new UnauthorizedDicomActionException(DataActions.Write); - - return await _updateInstanceOperationService.QueueUpdateOperationAsync(request.UpdateSpec, cancellationToken); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Update/UpdateInstanceOperationService.cs b/src/Microsoft.Health.Dicom.Core/Features/Update/UpdateInstanceOperationService.cs deleted file mode 100644 index 8683d18736..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Update/UpdateInstanceOperationService.cs +++ /dev/null @@ -1,116 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Linq; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using FellowOakDicom; -using Microsoft.ApplicationInsights; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Audit; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Context; -using Microsoft.Health.Dicom.Core.Features.Diagnostic; -using Microsoft.Health.Dicom.Core.Features.Operations; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Core.Messages.Update; -using Microsoft.Health.Dicom.Core.Models.Operations; -using Microsoft.Health.Dicom.Core.Models.Update; -using Microsoft.Health.Operations; - -namespace Microsoft.Health.Dicom.Core.Features.Update; - -public class UpdateInstanceOperationService : IUpdateInstanceOperationService -{ - private readonly IGuidFactory _guidFactory; - private readonly IDicomOperationsClient _client; - private readonly IDicomRequestContextAccessor _contextAccessor; - private readonly TelemetryClient _telemetryClient; - private readonly ILogger _logger; - private readonly IOptions _jsonSerializerOptions; - - private static readonly OperationQueryCondition Query = new OperationQueryCondition - { - Operations = new DicomOperation[] { DicomOperation.Update }, - Statuses = new OperationStatus[] - { - OperationStatus.NotStarted, - OperationStatus.Running, - } - }; - - public UpdateInstanceOperationService( - IGuidFactory guidFactory, - IDicomOperationsClient client, - IDicomRequestContextAccessor contextAccessor, - TelemetryClient telemetryClient, - IOptions jsonSerializerOptions, - ILogger logger) - { - EnsureArg.IsNotNull(guidFactory, nameof(guidFactory)); - EnsureArg.IsNotNull(client, nameof(client)); - EnsureArg.IsNotNull(contextAccessor, nameof(contextAccessor)); - EnsureArg.IsNotNull(telemetryClient, nameof(telemetryClient)); - EnsureArg.IsNotNull(jsonSerializerOptions?.Value, nameof(jsonSerializerOptions)); - EnsureArg.IsNotNull(logger, nameof(logger)); - - _guidFactory = guidFactory; - _client = client; - _contextAccessor = contextAccessor; - _telemetryClient = telemetryClient; - _logger = logger; - _jsonSerializerOptions = jsonSerializerOptions; - } - - public async Task QueueUpdateOperationAsync( - UpdateSpecification updateSpecification, - CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(updateSpecification, nameof(updateSpecification)); - EnsureArg.IsNotNull(updateSpecification.ChangeDataset, nameof(updateSpecification.ChangeDataset)); - - UpdateRequestValidator.ValidateRequest(updateSpecification); - DicomDataset failedSop = UpdateRequestValidator.ValidateDicomDataset(updateSpecification.ChangeDataset); - - if (failedSop.Any()) - { - return new UpdateInstanceResponse(failedSop); - } - - OperationReference activeOperation = await _client - .FindOperationsAsync(Query, cancellationToken) - .FirstOrDefaultAsync(cancellationToken); - - if (activeOperation != null) - throw new ExistingOperationException(activeOperation, "update"); - - Guid operationId = _guidFactory.Create(); - - EnsureArg.IsNotNull(updateSpecification, nameof(updateSpecification)); - EnsureArg.IsNotNull(updateSpecification.ChangeDataset, nameof(updateSpecification.ChangeDataset)); - - Partition partition = _contextAccessor.RequestContext.GetPartition(); - - try - { - var operation = await _client.StartUpdateOperationAsync(operationId, updateSpecification, partition, cancellationToken); - - string input = JsonSerializer.Serialize(updateSpecification, _jsonSerializerOptions.Value); - _telemetryClient.ForwardOperationLogTrace("Dicom update operation started successfully.", operationId.ToString(), input, AuditEventSubType.UpdateStudyOperation); - return new UpdateInstanceResponse(operation); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to start update operation"); - throw; - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Update/UpdateInstanceService.cs b/src/Microsoft.Health.Dicom.Core/Features/Update/UpdateInstanceService.cs deleted file mode 100644 index b3cd2b79f8..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Update/UpdateInstanceService.cs +++ /dev/null @@ -1,218 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using FellowOakDicom; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Core.Features.Retrieve; -using Microsoft.IO; - -namespace Microsoft.Health.Dicom.Core.Features.Update; - -public class UpdateInstanceService : IUpdateInstanceService -{ - private readonly IFileStore _fileStore; - private readonly IMetadataStore _metadataStore; - private readonly ILogger _logger; - private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager; - private readonly int _largeDicomItemSizeInBytes; - private readonly int _stageBlockSizeInBytes; - private const int LargeObjectSizeInBytes = 1000; - - public UpdateInstanceService( - IFileStore fileStore, - IMetadataStore metadataStore, - RecyclableMemoryStreamManager recyclableMemoryStreamManager, - IOptions updateConfiguration, - ILogger logger) - { - _fileStore = EnsureArg.IsNotNull(fileStore, nameof(fileStore)); - _metadataStore = EnsureArg.IsNotNull(metadataStore, nameof(metadataStore)); - _logger = EnsureArg.IsNotNull(logger, nameof(logger)); - _recyclableMemoryStreamManager = EnsureArg.IsNotNull(recyclableMemoryStreamManager, nameof(recyclableMemoryStreamManager)); - var configuration = EnsureArg.IsNotNull(updateConfiguration?.Value, nameof(updateConfiguration)); - _largeDicomItemSizeInBytes = configuration.LargeDicomItemsizeInBytes; - _stageBlockSizeInBytes = configuration.StageBlockSizeInBytes; - } - - /// - public async Task UpdateInstanceBlobAsync(InstanceMetadata instance, DicomDataset datasetToUpdate, Partition partition, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(datasetToUpdate, nameof(datasetToUpdate)); - EnsureArg.IsNotNull(instance, nameof(instance)); - EnsureArg.IsTrue(instance.InstanceProperties.NewVersion.HasValue, nameof(instance.InstanceProperties.NewVersion.HasValue)); - EnsureArg.IsNotNull(partition, nameof(partition)); - - Task updateInstanceFileTask = UpdateInstanceFileAsync(instance, datasetToUpdate, partition, cancellationToken); - await UpdateInstanceMetadataAsync(instance, datasetToUpdate, cancellationToken); - return await updateInstanceFileTask; - } - - /// - public async Task DeleteInstanceBlobAsync(long fileIdentifier, Partition partition, FileProperties fileProperties, CancellationToken cancellationToken = default) - { - EnsureArg.IsNotNull(partition, nameof(partition)); - _logger.LogInformation("Begin deleting instance blob {FileIdentifier}.", fileIdentifier); - - Task fileTask = _fileStore.DeleteFileIfExistsAsync(fileIdentifier, partition, fileProperties, cancellationToken); - Task metadataTask = _metadataStore.DeleteInstanceMetadataIfExistsAsync(fileIdentifier, cancellationToken); - - await Task.WhenAll(fileTask, metadataTask); - - _logger.LogInformation("Deleting instance blob {FileIdentifier} completed successfully.", fileIdentifier); - } - - private async Task UpdateInstanceFileAsync(InstanceMetadata instance, DicomDataset datasetToUpdate, Partition partition, CancellationToken cancellationToken) - { - long originFileIdentifier = instance.VersionedInstanceIdentifier.Version; - long newFileIdentifier = instance.InstanceProperties.NewVersion.Value; - var stopwatch = new Stopwatch(); - - KeyValuePair block = default; - FileProperties newFileProperties = default; - - bool isPreUpdated = instance.InstanceProperties.OriginalVersion.HasValue; - - // If the file is already updated, then we can copy the file and just update the first block - // We don't want to download the entire file, downloading the first block is sufficient to update patient data - if (isPreUpdated) - { - _logger.LogInformation("Begin copying instance file {OrignalFileIdentifier} - {NewFileIdentifier}", originFileIdentifier, newFileIdentifier); - await _fileStore.CopyFileAsync(originFileIdentifier, newFileIdentifier, partition, instance.InstanceProperties.FileProperties, cancellationToken); - newFileProperties = await _fileStore.GetFilePropertiesAsync(newFileIdentifier, partition, null, cancellationToken); - } - else - { - stopwatch.Start(); - _logger.LogInformation("Begin downloading original file {OrignalFileIdentifier} - {NewFileIdentifier}", originFileIdentifier, newFileIdentifier); - - // If not pre-updated get the file stream, GetFileAsync will open the stream - using Stream stream = await _fileStore.GetFileAsync(originFileIdentifier, partition, instance.InstanceProperties.FileProperties, cancellationToken); - - // Read the file and check if there is any large DICOM item in the file. - DicomFile dcmFile = await DicomFile.OpenAsync(stream, FileReadOption.ReadLargeOnDemand, LargeObjectSizeInBytes); - - long firstBlockLength = stream.Length; - - // If the file is greater than 1MB try to upload in different blocks. - // Since we are supporting updating only Patient demographic and identification module, - // we assume patient attributes are at the very beginning of the dataset. - // If in future, we support updating other attributes like private tags, which could appear at the end - // We need to read the whole file instead of first block. - if (stream.Length > _largeDicomItemSizeInBytes) - { - _logger.LogInformation("Found bigger DICOM item, splitting the file into multiple blocks. {OrignalFileIdentifier} - {NewFileIdentifier}", originFileIdentifier, newFileIdentifier); - - if (dcmFile.Dataset.TryGetLargeDicomItem(_largeDicomItemSizeInBytes, _stageBlockSizeInBytes, out DicomItem largeDicomItem)) - { - RemoveItemsAfter(dcmFile.Dataset, largeDicomItem.Tag); - } - - // Find first block length to upload in blocks - firstBlockLength = await dcmFile.GetByteLengthAsync(_recyclableMemoryStreamManager); - } - - // Retain the first block information to update the metadata information - block = new KeyValuePair(Convert.ToBase64String(Guid.NewGuid().ToByteArray()), firstBlockLength); - - _logger.LogInformation("Begin uploading instance file in blocks {OrignalFileIdentifier} - {NewFileIdentifier}", originFileIdentifier, newFileIdentifier); - - stream.Seek(0, SeekOrigin.Begin); - - // Copy the original file into another file as multiple blocks - newFileProperties = await _fileStore.StoreFileInBlocksAsync(newFileIdentifier, partition, stream, _stageBlockSizeInBytes, block, cancellationToken); - - stopwatch.Stop(); - - _logger.LogInformation("Uploading instance file in blocks {FileIdentifier} completed successfully. {TotalTimeTakenInMs} ms", newFileIdentifier, stopwatch.ElapsedMilliseconds); - } - - // Update the patient metadata only on the first block of data - return await UpdateDatasetInFileAsync(newFileIdentifier, datasetToUpdate, partition, newFileProperties, block, cancellationToken); - } - - private async Task UpdateInstanceMetadataAsync(InstanceMetadata instance, DicomDataset datasetToUpdate, CancellationToken cancellationToken) - { - long originFileIdentifier = instance.VersionedInstanceIdentifier.Version; - long newFileIdentifier = instance.InstanceProperties.NewVersion.Value; - - _logger.LogInformation("Begin downloading original file metdata {OrignalFileIdentifier} - {NewFileIdentifier}", originFileIdentifier, newFileIdentifier); - - DicomDataset metadata = await _metadataStore.GetInstanceMetadataAsync(originFileIdentifier, cancellationToken); - metadata.AddOrUpdate(datasetToUpdate); - - await _metadataStore.StoreInstanceMetadataAsync(metadata, newFileIdentifier, cancellationToken); - - _logger.LogInformation("Updating metadata file {OrignalFileIdentifier} - {NewFileIdentifier} completed successfully", originFileIdentifier, newFileIdentifier); - } - - private async Task UpdateDatasetInFileAsync(long newFileIdentifier, DicomDataset datasetToUpdate, Partition partition, FileProperties newFileProperties, KeyValuePair block = default, CancellationToken cancellationToken = default) - { - const string SrcTag = nameof(UpdateDatasetInFileAsync) + "-src"; - const string DestTag = nameof(UpdateDatasetInFileAsync) + "-dest"; - - var stopwatch = new Stopwatch(); - _logger.LogInformation("Begin updating new file {NewFileIdentifier}", newFileIdentifier); - - stopwatch.Start(); - - // If the block is not provided, then we need to get the first block to update the dataset - // This scenario occurs if the file is already updated and we have stored in multiple blocks - if (block.Key is null) - { - block = await _fileStore.GetFirstBlockPropertyAsync(newFileIdentifier, partition, newFileProperties, cancellationToken); - } - - BinaryData data = await _fileStore.GetFileContentInRangeAsync(newFileIdentifier, partition, newFileProperties, new FrameRange(0, block.Value), cancellationToken); - - using MemoryStream stream = _recyclableMemoryStreamManager.GetStream(tag: SrcTag, buffer: data); - DicomFile dicomFile = await DicomFile.OpenAsync(stream); - dicomFile.Dataset.AddOrUpdate(datasetToUpdate); - - using MemoryStream resultStream = _recyclableMemoryStreamManager.GetStream(tag: DestTag); - await dicomFile.SaveAsync(resultStream); - - FileProperties updatedFileProperties = await _fileStore.UpdateFileBlockAsync(newFileIdentifier, partition, newFileProperties, block.Key, resultStream, cancellationToken); - - stopwatch.Stop(); - - _logger.LogInformation("Updating new file {NewFileIdentifier} completed successfully. {TotalTimeTakenInMs} ms", newFileIdentifier, stopwatch.ElapsedMilliseconds); - return updatedFileProperties; - } - - // Removes all items in the list after the specified item tag - private static void RemoveItemsAfter(DicomDataset dataset, DicomTag tag) - { - bool toRemove = false; - var dicomTags = new List(); - - foreach (var item in dataset) - { - if (item.Tag == tag) - { - toRemove = true; - } - - if (toRemove) - { - dicomTags.Add(item.Tag); - } - } - - dataset.Remove(dicomTags.ToArray()); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Update/UpdateRequestValidator.cs b/src/Microsoft.Health.Dicom.Core/Features/Update/UpdateRequestValidator.cs deleted file mode 100644 index bc840538c7..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Update/UpdateRequestValidator.cs +++ /dev/null @@ -1,84 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using EnsureThat; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Store; -using Microsoft.Health.Dicom.Core.Features.Validation; -using Microsoft.Health.Dicom.Core.Messages.Update; -using Microsoft.Health.Dicom.Core.Models.Update; - -namespace Microsoft.Health.Dicom.Core.Features.Update; - -/// -/// Provides functionality to validate an . -/// -public static class UpdateRequestValidator -{ - /// - /// Validates an . - /// - /// The request to validate. - /// Thrown when request body is missing. - /// Thrown when the specified StudyInstanceUID is not a valid identifier. - public static void ValidateRequest(UpdateSpecification updateSpecification) - { - EnsureArg.IsNotNull(updateSpecification, nameof(updateSpecification)); - if (updateSpecification.StudyInstanceUids == null || updateSpecification.StudyInstanceUids.Count == 0) - { - throw new BadRequestException(string.Format(CultureInfo.CurrentCulture, DicomCoreResource.MissingRequiredField, nameof(updateSpecification.StudyInstanceUids))); - } - else if (updateSpecification.StudyInstanceUids.Count > UpdateTags.MaxStudyInstanceUidLimit) - { - throw new BadRequestException(string.Format(CultureInfo.CurrentCulture, DicomCoreResource.DicomUpdateStudyInstanceUidsExceedMaxCount, UpdateTags.MaxStudyInstanceUidLimit)); - } - foreach (var StudyInstanceUid in updateSpecification.StudyInstanceUids) - { - UidValidation.Validate(StudyInstanceUid, nameof(StudyInstanceUid)); - } - } - - /// - /// Validates a . - /// - /// The Dicom dataset to validate. - /// Thrown when dicom tag or value validation fails - public static DicomDataset ValidateDicomDataset(DicomDataset dataset) - { - EnsureArg.IsNotNull(dataset, nameof(dataset)); - var errors = new List(); - foreach (DicomItem item in dataset) - { - if (!UpdateTags.UpdateStudyFilterTags.Contains(item.Tag)) - { - var message = string.Format(CultureInfo.CurrentCulture, DicomCoreResource.DicomUpdateTagValidationFailed, item.Tag.ToString()); - errors.Add(string.Format(CultureInfo.InvariantCulture, DicomCoreResource.ErrorMessageFormat, ErrorNumbers.ValidationFailure, item.Tag.ToString(), message)); - } - else - { - try - { - item.Validate(); - } - catch (DicomValidationException ex) - { - errors.Add(string.Format(CultureInfo.InvariantCulture, DicomCoreResource.ErrorMessageFormat, ErrorNumbers.ValidationFailure, item.Tag.ToString(), ex.Message)); - } - } - } - var failedSop = new DicomDataset(); - if (errors.Count > 0) - { - var failedAttributes = errors.Select(error => new DicomDataset(new DicomLongString(DicomTag.ErrorComment, error))).ToArray(); - var failedAttributeSequence = new DicomSequence(DicomTag.FailedAttributesSequence, failedAttributes); - failedSop.Add(failedAttributeSequence); - } - return failedSop; - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Update/UpdateTags.cs b/src/Microsoft.Health.Dicom.Core/Features/Update/UpdateTags.cs deleted file mode 100644 index 0f3771b558..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Update/UpdateTags.cs +++ /dev/null @@ -1,54 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using FellowOakDicom; - -namespace Microsoft.Health.Dicom.Core.Features.Update; - -internal static class UpdateTags -{ - internal const int MaxStudyInstanceUidLimit = 50; - - internal static readonly HashSet UpdateStudyFilterTags = new HashSet() - { - DicomTag.PatientName, - DicomTag.PatientID, - DicomTag.OtherPatientIDsRETIRED, - DicomTag.TypeOfPatientID, - DicomTag.OtherPatientNames, - DicomTag.PatientBirthName, - DicomTag.PatientMotherBirthName, - DicomTag.MedicalRecordLocatorRETIRED, - DicomTag.PatientAge, - DicomTag.Occupation, - DicomTag.ConfidentialityConstraintOnPatientDataDescription, - DicomTag.PatientBirthDate, - DicomTag.PatientBirthTime, - DicomTag.PatientSex, - DicomTag.QualityControlSubject, - DicomTag.PatientSize, - DicomTag.PatientWeight, - DicomTag.PatientAddress, - DicomTag.MilitaryRank, - DicomTag.BranchOfService, - DicomTag.CountryOfResidence, - DicomTag.RegionOfResidence, - DicomTag.PatientTelephoneNumbers, - DicomTag.EthnicGroup, - DicomTag.PatientReligiousPreference, - DicomTag.PatientComments, - DicomTag.ResponsiblePerson, - DicomTag.ResponsiblePersonRole, - DicomTag.ResponsibleOrganization, - DicomTag.PatientSpeciesDescription, - DicomTag.PatientBreedDescription, - DicomTag.BreedRegistrationNumber, - DicomTag.IssuerOfPatientID, - DicomTag.AccessionNumber, - DicomTag.ReferringPhysicianName, - DicomTag.StudyDescription, - }; -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Validation/DateValidation.cs b/src/Microsoft.Health.Dicom.Core/Features/Validation/DateValidation.cs deleted file mode 100644 index 66d38a10f6..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Validation/DateValidation.cs +++ /dev/null @@ -1,25 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Globalization; -using FellowOakDicom; -using FellowOakDicom.IO.Buffer; -using Microsoft.Health.Dicom.Core.Exceptions; - -namespace Microsoft.Health.Dicom.Core.Features.Validation; - -internal class DateValidation : StringElementValidation -{ - private const string DateFormatDA = "yyyyMMdd"; - - protected override void ValidateStringElement(string name, DicomVR vr, string value, IByteBuffer buffer) - { - if (!DateTime.TryParseExact(value, DateFormatDA, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out _)) - { - throw new ElementValidationException(name, DicomVR.DA, ValidationErrorCode.DateIsInvalid); - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Validation/ElementMaxLengthValidation.cs b/src/Microsoft.Health.Dicom.Core/Features/Validation/ElementMaxLengthValidation.cs deleted file mode 100644 index 2c8ca50bb0..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Validation/ElementMaxLengthValidation.cs +++ /dev/null @@ -1,43 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Diagnostics; -using System.Globalization; -using EnsureThat; -using FellowOakDicom; -using FellowOakDicom.IO.Buffer; -using Microsoft.Health.Dicom.Core.Exceptions; - -namespace Microsoft.Health.Dicom.Core.Features.Validation; - -internal class ElementMaxLengthValidation : StringElementValidation -{ - public ElementMaxLengthValidation(int maxLength) - { - Debug.Assert(maxLength > 0, "MaxLength should be positive number."); - MaxLength = maxLength; - } - - public int MaxLength { get; } - - protected override void ValidateStringElement(string name, DicomVR vr, string value, IByteBuffer buffer) - { - Validate(value, MaxLength, name, vr); - } - - public static void Validate(string value, int maxLength, string name, DicomVR vr) - { - EnsureArg.IsNotNullOrEmpty(name, nameof(name)); - EnsureArg.IsNotNull(vr, nameof(vr)); - if (value?.Length > maxLength) - { - throw new ElementValidationException( - name, - vr, - ValidationErrorCode.ExceedMaxLength, - string.Format(CultureInfo.CurrentCulture, DicomCoreResource.ErrorMessageExceedMaxLength, maxLength)); - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Validation/ElementMinimumValidator.cs b/src/Microsoft.Health.Dicom.Core/Features/Validation/ElementMinimumValidator.cs deleted file mode 100644 index e25cc83770..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Validation/ElementMinimumValidator.cs +++ /dev/null @@ -1,55 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics; -using EnsureThat; -using FellowOakDicom; - -namespace Microsoft.Health.Dicom.Core.Features.Validation; - -public class ElementMinimumValidator : IElementMinimumValidator -{ - private static readonly ImmutableDictionary Validations = ImmutableDictionary.CreateRange( - new KeyValuePair[] - { - new(DicomVR.AE, new ElementMaxLengthValidation(16)), - new(DicomVR.AS, new ElementRequiredLengthValidation(4)), - new(DicomVR.CS, new ElementMaxLengthValidation(16)), - new(DicomVR.DA, new DateValidation()), - new(DicomVR.DT, new EncodedStringElementValidation()), - new(DicomVR.FD, new ElementRequiredLengthValidation(8)), - new(DicomVR.FL, new ElementRequiredLengthValidation(4)), - new(DicomVR.IS, new EncodedStringElementValidation()), - new(DicomVR.LO, new LongStringValidation()), - new(DicomVR.PN, new PersonNameValidation()), - new(DicomVR.SH, new ElementMaxLengthValidation(16)), - new(DicomVR.SL, new ElementRequiredLengthValidation(4)), - new(DicomVR.SS, new ElementRequiredLengthValidation(2)), - new(DicomVR.TM, new EncodedStringElementValidation()), - new(DicomVR.UI, new UidValidation()), - new(DicomVR.UL, new ElementRequiredLengthValidation(4)), - new(DicomVR.US, new ElementRequiredLengthValidation(2)), - }); - - public void Validate(DicomElement dicomElement, ValidationLevel validationLevel = ValidationLevel.Strict) - { - EnsureArg.IsNotNull(dicomElement, nameof(dicomElement)); - DicomVR vr = dicomElement.ValueRepresentation; - if (vr == null) - { - Debug.Fail("Dicom VR type should not be null"); - } - if (Validations.TryGetValue(vr, out IElementValidation validationRule)) - { - validationRule.Validate(dicomElement, validationLevel); - } - else - { - Debug.Fail($"Validating VR {vr?.Code} is not supported."); - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Validation/ElementRequiredLengthValidation.cs b/src/Microsoft.Health.Dicom.Core/Features/Validation/ElementRequiredLengthValidation.cs deleted file mode 100644 index 044dc0da16..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Validation/ElementRequiredLengthValidation.cs +++ /dev/null @@ -1,90 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; -using FellowOakDicom; -using FellowOakDicom.IO.Buffer; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Extensions; - -namespace Microsoft.Health.Dicom.Core.Features.Validation; - -internal class ElementRequiredLengthValidation : StringElementValidation -{ - protected override bool AllowNullOrEmpty => false; - - private static readonly HashSet StringVrs = new() - { - DicomVR.AE, - DicomVR.AS, - DicomVR.CS, - DicomVR.DA, - DicomVR.DS, - DicomVR.IS, - DicomVR.LO, - DicomVR.PN, - DicomVR.SH, - DicomVR.UI, - }; - - public int ExpectedLength { get; } - - public ElementRequiredLengthValidation(int expectedLength) - { - Debug.Assert(expectedLength >= 0, "Expected Length should be none-negative"); - ExpectedLength = expectedLength; - } - - protected override void ValidateStringElement(string name, DicomVR vr, string value, IByteBuffer buffer) - { - if (!String.IsNullOrEmpty(value)) - { - ValidateStringLength(vr, name, value); - } - else - { - ValidateByteBufferLength(vr, name, buffer); - } - } - - private void ValidateByteBufferLength(DicomVR dicomVR, string name, IByteBuffer value) - { - // We only validate first value, as long as long value.Size>=ExpectedLength, we are good to go. - if (value == null || value.Size == 0 || value.Size < ExpectedLength) - { - throw new ElementValidationException( - name, - dicomVR, - ValidationErrorCode.UnexpectedLength, - string.Format(CultureInfo.InvariantCulture, DicomCoreResource.ErrorMessageUnexpectedLength, ExpectedLength)); - } - } - - protected override string GetValueOrDefault(DicomElement dicomElement) - { - if (StringVrs.Contains(dicomElement.ValueRepresentation)) - { - // Only validate the first element - return dicomElement.GetFirstValueOrDefault(); - } - return string.Empty; - } - - private void ValidateStringLength(DicomVR dicomVR, string name, string value) - { - value ??= ""; - if (value.Length != ExpectedLength) - { - throw new ElementValidationException( - name, - dicomVR, - ValidationErrorCode.UnexpectedLength, - string.Format(CultureInfo.InvariantCulture, DicomCoreResource.ErrorMessageUnexpectedLength, ExpectedLength)); - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Validation/EncodedStringElementValidation.cs b/src/Microsoft.Health.Dicom.Core/Features/Validation/EncodedStringElementValidation.cs deleted file mode 100644 index c303f18068..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Validation/EncodedStringElementValidation.cs +++ /dev/null @@ -1,44 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using FellowOakDicom; -using FellowOakDicom.IO.Buffer; -using Microsoft.Health.Dicom.Core.Exceptions; - -namespace Microsoft.Health.Dicom.Core.Features.Validation; - -internal class EncodedStringElementValidation : StringElementValidation -{ - protected override void ValidateStringElement(string name, DicomVR vr, string value, IByteBuffer buffer) - { - switch (vr.Code) - { - case DicomVRCode.DT: - Validate(name, value, vr, buffer, DicomValidation.ValidateDT, ValidationErrorCode.DateTimeIsInvalid); - break; - case DicomVRCode.IS: - Validate(name, value, vr, buffer, DicomValidation.ValidateIS, ValidationErrorCode.IntegerStringIsInvalid); - break; - case DicomVRCode.TM: - Validate(name, value, vr, buffer, DicomValidation.ValidateTM, ValidationErrorCode.TimeIsInvalid); - break; - default: - throw new ArgumentOutOfRangeException(nameof(name)); - }; - } - - private static void Validate(string name, string value, DicomVR vr, IByteBuffer buffer, Action validate, ValidationErrorCode errorCode) - { - try - { - validate(value); - } - catch (DicomValidationException) - { - throw new ElementValidationException(name, vr, errorCode); - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Validation/IElementMinimumValidator.cs b/src/Microsoft.Health.Dicom.Core/Features/Validation/IElementMinimumValidator.cs deleted file mode 100644 index 6f2d4205e5..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Validation/IElementMinimumValidator.cs +++ /dev/null @@ -1,23 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Exceptions; - -namespace Microsoft.Health.Dicom.Core.Features.Validation; - -/// -/// Minimum validator on Dicom Element -/// -public interface IElementMinimumValidator -{ - /// - /// Validate Dicom Element. - /// - /// The Dicom Element - /// Style of validation to enforce on running rules - /// - void Validate(DicomElement dicomElement, ValidationLevel validationLevel = ValidationLevel.Strict); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Validation/IElementValidation.cs b/src/Microsoft.Health.Dicom.Core/Features/Validation/IElementValidation.cs deleted file mode 100644 index 3279357a92..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Validation/IElementValidation.cs +++ /dev/null @@ -1,23 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Exceptions; - -namespace Microsoft.Health.Dicom.Core.Features.Validation; - -/// -/// Validation on Dicom Element. -/// -internal interface IElementValidation -{ - /// - /// Validate DicomElement - /// - /// The dicom element - /// Validate with specific style - strict or more leninent/default - /// - void Validate(DicomElement dicomElement, ValidationLevel validationLevel = ValidationLevel.Strict); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Validation/ImplicitValueRepresentationValidator.cs b/src/Microsoft.Health.Dicom.Core/Features/Validation/ImplicitValueRepresentationValidator.cs deleted file mode 100644 index 956a14a036..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Validation/ImplicitValueRepresentationValidator.cs +++ /dev/null @@ -1,20 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using FellowOakDicom; - -namespace Microsoft.Health.Dicom.Core.Features.Validation; - -public static class ImplicitValueRepresentationValidator -{ - public static bool IsImplicitVR(DicomDataset dicomDataset) - { - EnsureArg.IsNotNull(dicomDataset, nameof(dicomDataset)); - EnsureArg.IsNotNull(dicomDataset.InternalTransferSyntax, nameof(dicomDataset.InternalTransferSyntax)); - - return !dicomDataset.InternalTransferSyntax.IsExplicitVR; - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Validation/LongStringValidation.cs b/src/Microsoft.Health.Dicom.Core/Features/Validation/LongStringValidation.cs deleted file mode 100644 index 3e50bedfbd..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Validation/LongStringValidation.cs +++ /dev/null @@ -1,38 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using FellowOakDicom; -using FellowOakDicom.IO.Buffer; -using Microsoft.Health.Dicom.Core.Exceptions; - -namespace Microsoft.Health.Dicom.Core.Features.Validation; - -/// -/// Validate Dicom VR LO -/// -internal class LongStringValidation : StringElementValidation -{ - protected override void ValidateStringElement(string name, DicomVR vr, string value, IByteBuffer buffer) - { - Validate(value, name); - } - - public static void Validate(string value, string name) - { - if (string.IsNullOrEmpty(value)) - { - return; - } - - ElementMaxLengthValidation.Validate(value, 64, name, DicomVR.LO); - - if (value.Contains('\\', StringComparison.OrdinalIgnoreCase) || ValidationUtils.ContainsControlExceptEsc(value)) - { - throw new ElementValidationException(name, DicomVR.LO, ValidationErrorCode.InvalidCharacters); - } - } -} - diff --git a/src/Microsoft.Health.Dicom.Core/Features/Validation/PartitionNameValidator.cs b/src/Microsoft.Health.Dicom.Core/Features/Validation/PartitionNameValidator.cs deleted file mode 100644 index 48757d65ca..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Validation/PartitionNameValidator.cs +++ /dev/null @@ -1,27 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Text.RegularExpressions; -using Microsoft.Health.Dicom.Core.Exceptions; - -namespace Microsoft.Health.Dicom.Core.Features.Validation; - -public static class PartitionNameValidator -{ - private static readonly Regex ValidIdentifierCharactersFormat = new Regex("^[A-Za-z0-9_.-]*$", RegexOptions.Compiled); - - public static void Validate(string value) - { - if (string.IsNullOrWhiteSpace(value)) - { - throw new InvalidPartitionNameException(); - } - - if (value.Length > 64 || !ValidIdentifierCharactersFormat.IsMatch(value)) - { - throw new InvalidPartitionNameException(); - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Validation/PersonNameValidation.cs b/src/Microsoft.Health.Dicom.Core/Features/Validation/PersonNameValidation.cs deleted file mode 100644 index e52f04699d..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Validation/PersonNameValidation.cs +++ /dev/null @@ -1,46 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Linq; -using FellowOakDicom; -using FellowOakDicom.IO.Buffer; -using Microsoft.Health.Dicom.Core.Exceptions; - -namespace Microsoft.Health.Dicom.Core.Features.Validation; - -internal class PersonNameValidation : StringElementValidation -{ - protected override void ValidateStringElement(string name, DicomVR vr, string value, IByteBuffer buffer) - { - string[] groups = value.Split('='); - if (groups.Length > 3) - { - throw new ElementValidationException(name, DicomVR.PN, ValidationErrorCode.PersonNameExceedMaxGroups); - } - - foreach (string group in groups) - { - try - { - ElementMaxLengthValidation.Validate(group, 64, name, vr); - } - catch (ElementValidationException ex) when (ex.ErrorCode == ValidationErrorCode.ExceedMaxLength) - { - // Reprocess the exception to make more meaningful message - throw new ElementValidationException(name, DicomVR.PN, ValidationErrorCode.PersonNameGroupExceedMaxLength); - } - - if (ValidationUtils.ContainsControlExceptEsc(group)) - { - throw new ElementValidationException(name, vr, ValidationErrorCode.InvalidCharacters); - } - } - - if (groups.Select(g => g.Split('^').Length).Any(l => l > 5)) - { - throw new ElementValidationException(name, DicomVR.PN, ValidationErrorCode.PersonNameExceedMaxComponents); - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Validation/StringElementValidation.cs b/src/Microsoft.Health.Dicom.Core/Features/Validation/StringElementValidation.cs deleted file mode 100644 index bbcd73bc10..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Validation/StringElementValidation.cs +++ /dev/null @@ -1,41 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using FellowOakDicom; -using FellowOakDicom.IO.Buffer; -using Microsoft.Health.Dicom.Core.Extensions; - -namespace Microsoft.Health.Dicom.Core.Features.Validation; - - -internal abstract class StringElementValidation : IElementValidation -{ - protected virtual bool AllowNullOrEmpty => true; - - public void Validate(DicomElement dicomElement, ValidationLevel validationLevel = ValidationLevel.Default) - { - EnsureArg.IsNotNull(dicomElement, nameof(dicomElement)); - - string name = dicomElement.Tag.GetFriendlyName(); - string value = GetValueOrDefault(dicomElement); - - if (!string.IsNullOrEmpty(value) && validationLevel == ValidationLevel.Default) - value = value.TrimEnd('\0'); - - // By default we will allow null or empty string and not go further with validation - if (AllowNullOrEmpty && string.IsNullOrEmpty(value)) - return; - - ValidateStringElement(name, dicomElement.ValueRepresentation, value, dicomElement.Buffer); - } - - protected abstract void ValidateStringElement(string name, DicomVR vr, string value, IByteBuffer buffer); - - protected virtual string GetValueOrDefault(DicomElement dicomElement) - { - return dicomElement.GetFirstValueOrDefault(); - } -} \ No newline at end of file diff --git a/src/Microsoft.Health.Dicom.Core/Features/Validation/UidValidation.cs b/src/Microsoft.Health.Dicom.Core/Features/Validation/UidValidation.cs deleted file mode 100644 index 9af66ef62b..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Validation/UidValidation.cs +++ /dev/null @@ -1,51 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Text.RegularExpressions; -using FellowOakDicom; -using FellowOakDicom.IO.Buffer; -using Microsoft.Health.Dicom.Core.Exceptions; - -namespace Microsoft.Health.Dicom.Core.Features.Validation; - -internal partial class UidValidation : StringElementValidation -{ - // Note: This regex allows for leading zeroes. - // A more strict regex may look like: "^(0|([1-9][0-9]*))(\\.(0|([1-9][0-9]*)))*$" -#if NET7_0_OR_GREATER - [GeneratedRegex("^[0-9\\.]*[0-9]$", RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture | RegexOptions.Singleline)] - private static partial Regex UidRegex(); -#else - private static readonly Regex UidRegex = new("^[0-9\\.]*[0-9]$", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture | RegexOptions.Singleline); -#endif - - protected override void ValidateStringElement(string name, DicomVR vr, string value, IByteBuffer buffer) - => Validate(value, name, allowEmpty: true); - - public static bool IsValid(string value, bool allowEmpty = false) - { - if (string.IsNullOrEmpty(value)) - return allowEmpty; - - // trailling spaces are allowed - value = value.TrimEnd(' '); - -#if NET7_0_OR_GREATER - if (value.Length > 64 || !UidRegex().IsMatch(value)) -#else - if (value.Length > 64 || !UidRegex.IsMatch(value)) -#endif - return false; - - return true; - } - - public static void Validate(string value, string name, bool allowEmpty = false) - { - // UI value is validated in other cases like params for WADO, DELETE. So keeping the exception specific. - if (!IsValid(value, allowEmpty)) - throw new InvalidIdentifierException(name); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Validation/ValidationErrorCode.cs b/src/Microsoft.Health.Dicom.Core/Features/Validation/ValidationErrorCode.cs deleted file mode 100644 index 8419c10d69..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Validation/ValidationErrorCode.cs +++ /dev/null @@ -1,138 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.Health.Dicom.Core.Features.Validation; - -/// -/// Represents a problem with the value for a given DICOM Value Representation (VR). -/// -/// -/// Error Code is smallint/short ranging between [0, 32,767]. -/// For convenience, codes are grouped together by VR where the first 1000 values are agnostic of VR. -/// By convention, each VR-specific error code should start with the VR name. -/// e.g. starts with PersonName. -/// -[SuppressMessage(category: "Design", checkId: "CA1028: Enum Storage should be Int32", Justification = "Value is stored in SQL as SMALLINT")] -public enum ValidationErrorCode : short -{ - #region General - - /// - /// No error - /// - None = 0, - - /// - /// The dicom element has multiple values. - /// - MultipleValues = 1, - - /// - /// The length of dicom element value exceed max allowed. - /// - ExceedMaxLength = 2, - - /// - /// The length of dicom element value is not expected. - /// - UnexpectedLength = 3, - - /// - /// The dicom element value has invalid characters. - /// - InvalidCharacters = 4, - - /// - /// The VR of dicom element is not expected. - /// - UnexpectedVR = 5, - - /// - /// Implicit VR Transfer Syntax is not allowed. - /// - ImplicitVRNotAllowed = 6, - - #endregion - - #region Person Name (PN) - - /// - /// Person name element has more than allowed groups. - /// - PersonNameExceedMaxGroups = 1000, - - /// - /// The length of person name group exceed max allowed. - /// - PersonNameGroupExceedMaxLength = 1001, - - /// - /// Person name element has more than allowed component. - /// - PersonNameExceedMaxComponents = 1002, - - #endregion - - #region Date (DA) - - /// - /// Date element has invalid value. - /// - DateIsInvalid = 1100, - - #endregion - - #region Unique Identifier (UI) - - /// - /// Uid element has invalid value. - /// - UidIsInvalid = 1200, - - #endregion - - #region Date Time (DT) - - /// - /// Date Time element has invalid value. - /// - DateTimeIsInvalid = 1300, - - #endregion - - #region Time (TM) - - /// - /// Time element has invalid value. - /// - TimeIsInvalid = 1400, - - #endregion - - #region Integer String (IS) - - /// - /// Integer String element has an invalid value. - /// - IntegerStringIsInvalid = 1500, - - #endregion - - #region Sequence of Items (SQ) - - /// - /// Sequences are not allowed. - /// - SequenceDisallowed = 1600, - - /// - /// Nested sequences are not allowed. - /// - NestedSequence = 1601, - - #endregion -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Validation/ValidationLevel.cs b/src/Microsoft.Health.Dicom.Core/Features/Validation/ValidationLevel.cs deleted file mode 100644 index 333b5427e5..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Validation/ValidationLevel.cs +++ /dev/null @@ -1,23 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Features.Validation; - -/// -/// A set of validation levels to pick from to run validations against -/// -public enum ValidationLevel -{ - /// - /// Default takes a more lenient approach with strings when running validation rules. It drops null padding on a string and then continues to - /// perform all validation after. - /// - Default, - - /// - /// Strict validation currently fails when a string value has null padding - /// - Strict -} \ No newline at end of file diff --git a/src/Microsoft.Health.Dicom.Core/Features/Validation/ValidationUtils.cs b/src/Microsoft.Health.Dicom.Core/Features/Validation/ValidationUtils.cs deleted file mode 100644 index fa34d61df4..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Validation/ValidationUtils.cs +++ /dev/null @@ -1,13 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Linq; - -namespace Microsoft.Health.Dicom.Core.Features.Validation; -internal static class ValidationUtils -{ - public static bool ContainsControlExceptEsc(string text) - => text != null && text.Any(c => char.IsControl(c) && (c != '\u001b')); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Workitem/AddWorkitemRequestHandler.cs b/src/Microsoft.Health.Dicom.Core/Features/Workitem/AddWorkitemRequestHandler.cs deleted file mode 100644 index 23068161ac..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Workitem/AddWorkitemRequestHandler.cs +++ /dev/null @@ -1,48 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using MediatR; -using Microsoft.Health.Core.Features.Security.Authorization; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Security; -using Microsoft.Health.Dicom.Core.Messages.Workitem; - -namespace Microsoft.Health.Dicom.Core.Features.Workitem; - -public class AddWorkitemRequestHandler : BaseHandler, IRequestHandler -{ - private readonly IWorkitemService _workItemService; - - public AddWorkitemRequestHandler( - IAuthorizationService authorizationService, - IWorkitemService workItemService) - : base(authorizationService) - { - _workItemService = EnsureArg.IsNotNull(workItemService, nameof(workItemService)); - } - - /// - public async Task Handle( - AddWorkitemRequest request, - CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(request, nameof(request)); - - if (await AuthorizationService.CheckAccess(DataActions.Write, cancellationToken).ConfigureAwait(false) != DataActions.Write) - { - throw new UnauthorizedDicomActionException(DataActions.Write); - } - - request.Validate(); - - return await _workItemService - .ProcessAddAsync(request.DicomDataset, request.WorkitemInstanceUid, cancellationToken) - .ConfigureAwait(false); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Workitem/CancelWorkitemRequestHandler.cs b/src/Microsoft.Health.Dicom.Core/Features/Workitem/CancelWorkitemRequestHandler.cs deleted file mode 100644 index 1062c566a0..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Workitem/CancelWorkitemRequestHandler.cs +++ /dev/null @@ -1,46 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using MediatR; -using Microsoft.Health.Core.Features.Security.Authorization; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Security; -using Microsoft.Health.Dicom.Core.Messages.Workitem; - -namespace Microsoft.Health.Dicom.Core.Features.Workitem; - -public class CancelWorkitemRequestHandler : BaseHandler, IRequestHandler -{ - private readonly IWorkitemService _workItemService; - - public CancelWorkitemRequestHandler( - IAuthorizationService authorizationService, - IWorkitemService workItemService) - : base(authorizationService) - { - _workItemService = EnsureArg.IsNotNull(workItemService, nameof(workItemService)); - } - - /// - public async Task Handle(CancelWorkitemRequest request, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(request, nameof(request)); - - if (await AuthorizationService.CheckAccess(DataActions.Write, cancellationToken).ConfigureAwait(false) != DataActions.Write) - { - throw new UnauthorizedDicomActionException(DataActions.Write); - } - - request.Validate(); - - return await _workItemService - .ProcessCancelAsync(request.DicomDataset, request.WorkitemInstanceUid, cancellationToken) - .ConfigureAwait(false); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Workitem/ChangeWorkitemStateRequestHandler.cs b/src/Microsoft.Health.Dicom.Core/Features/Workitem/ChangeWorkitemStateRequestHandler.cs deleted file mode 100644 index fce65b24e7..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Workitem/ChangeWorkitemStateRequestHandler.cs +++ /dev/null @@ -1,46 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using MediatR; -using Microsoft.Health.Core.Features.Security.Authorization; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Security; -using Microsoft.Health.Dicom.Core.Messages.Workitem; - -namespace Microsoft.Health.Dicom.Core.Features.Workitem; - -public class ChangeWorkitemStateRequestHandler : BaseHandler, IRequestHandler -{ - private readonly IWorkitemService _workItemService; - - public ChangeWorkitemStateRequestHandler( - IAuthorizationService authorizationService, - IWorkitemService workItemService) - : base(authorizationService) - { - _workItemService = EnsureArg.IsNotNull(workItemService, nameof(workItemService)); - } - - /// - public async Task Handle(ChangeWorkitemStateRequest request, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(request, nameof(request)); - - if (await AuthorizationService.CheckAccess(DataActions.Write, cancellationToken).ConfigureAwait(false) != DataActions.Write) - { - throw new UnauthorizedDicomActionException(DataActions.Write); - } - - request.Validate(); - - return await _workItemService - .ProcessChangeStateAsync(request.DicomDataset, request.WorkitemInstanceUid, cancellationToken) - .ConfigureAwait(false); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Workitem/IIndexWorkitemStore.cs b/src/Microsoft.Health.Dicom.Core/Features/Workitem/IIndexWorkitemStore.cs deleted file mode 100644 index c141d3c7f2..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Workitem/IIndexWorkitemStore.cs +++ /dev/null @@ -1,114 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Features.Workitem.Model; -using Microsoft.Health.Dicom.Core.Features.Query.Model; - -namespace Microsoft.Health.Dicom.Core.Features.Workitem; - -/// -/// Provides functionality to index UPS-RS workitems. -/// -public interface IIndexWorkitemStore -{ - /// - /// Asynchronously begin the creation of a workitem instance. - /// - /// The partition key. - /// The DICOM dataset to index. - /// Queryable workitem tags - /// The cancellation token. - /// A task that gets the workitem identifier. - Task BeginAddWorkitemAsync(int partitionKey, DicomDataset dataset, IEnumerable queryTags, CancellationToken cancellationToken = default); - - /// - /// Asynchronously completes the creation of a workitem instance. - /// - /// The Partition Key - /// The workitem instance key. - /// The cancellation token. - /// A task that represents the asynchronous update operation. - Task EndAddWorkitemAsync(int partitionKey, long workitemKey, CancellationToken cancellationToken = default); - - /// - /// Asynchronously updates the workitem instance status. - /// - /// The Partition Key - /// The workitem instance key. - /// The Workitem status - /// The cancellation token. - /// A task representing asynchronous update workitem status operation. - Task UpdateWorkitemStatusAsync(int partitionKey, long workitemKey, WorkitemStoreStatus status, CancellationToken cancellationToken = default); - - /// - /// Asynchronously updates the workitem instance's Procedure Step State. - /// - /// The Workitem Metadata - /// The Proposed Watermark - /// The Procedure Step State - /// The Workitem Transaction UID - /// The cancellation token. - /// A Task representing the method status. - Task UpdateWorkitemProcedureStepStateAsync(WorkitemMetadataStoreEntry workitemMetadata, long proposedWatermark, string procedureStepState, string transactionUid, CancellationToken cancellationToken = default); - - /// - /// Asynchronously updates the workitem. - /// Update workitem with the new watermak. - /// Update details in extended query tag tables. - /// - /// - /// - /// - /// - /// - /// A task representing asynchronous update workitem operation. - Task UpdateWorkitemTransactionAsync(WorkitemMetadataStoreEntry workitemMetadata, long proposedWatermark, DicomDataset dataset, IEnumerable queryTags, CancellationToken cancellationToken = default); - - /// - /// Asynchronously deletes a workitem instance. - /// - /// The Workitem Identifier. - /// The cancellation token. - /// A Task representing the method status. - Task DeleteWorkitemAsync(WorkitemInstanceIdentifier identifier, CancellationToken cancellationToken = default); - - /// - /// Asynchronously gets workitem query tags - /// - /// - /// A task that gets workitem query tags. - Task> GetWorkitemQueryTagsAsync(CancellationToken cancellationToken = default); - - /// - /// Asynchronously gets workitem metadata - /// - /// The Partition key - /// The workitem instance UID. - /// The cancellation token - /// Returns the Workitem attributes that are indexed in a store. - Task GetWorkitemMetadataAsync(int partitionKey, string workitemUid, CancellationToken cancellationToken = default); - - /// - /// Asynchronously gets current and next workitem watermark - /// - /// The workitem key. - /// The cancellation token - /// Returns the Current and Next Workitem Watermark. - Task<(long CurrentWatermark, long NextWatermark)?> GetCurrentAndNextWorkitemWatermarkAsync(long workitemKey, CancellationToken cancellationToken = default); - - /// - /// Asynchronously queries workitem - /// - /// The partition key. - /// Query expression that matches the filters - /// The cancellation token. - /// A task that gets workitem that matches the query filters - Task QueryAsync(int partitionKey, BaseQueryExpression query, CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Workitem/IWorkItemService.cs b/src/Microsoft.Health.Dicom.Core/Features/Workitem/IWorkItemService.cs deleted file mode 100644 index 97080dd187..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Workitem/IWorkItemService.cs +++ /dev/null @@ -1,92 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Features.Query; -using Microsoft.Health.Dicom.Core.Messages.Workitem; - -namespace Microsoft.Health.Dicom.Core.Features.Workitem; - -/// -/// Provides functionality to process the . -/// -public interface IWorkitemService -{ - /// - /// Asynchronously processes the workitem dataset - /// - /// - /// If the is not specified, a new workitemInstanceUid is created. - /// - /// The to process. - /// An optional value for the Work Item InstanceUID tag. - /// The cancellation token. - /// A task that represents the asynchronous process operation. - Task ProcessAddAsync( - DicomDataset dataset, - string workitemInstanceUid, - CancellationToken cancellationToken); - - /// - /// Asynchronously processes the Cancel workitem dataset - /// - /// The to process. - /// The Work Item InstanceUID tag. - /// The cancellation token. - /// A task that represents the asynchronous process operation. - Task ProcessCancelAsync( - DicomDataset dataset, - string workitemInstanceUid, - CancellationToken cancellationToken); - - /// - /// Asynchronously processes the retrieval of a UPS-RS workitem - /// - /// The workitem instance UID - /// The cancellation token - /// A task that represents the asynchronous process operation. - Task ProcessRetrieveAsync(string workitemInstanceUid, CancellationToken cancellationToken); - - /// - /// Asynchronously process the searching of a UPS-RS workitem - /// - /// Query parameters that contains filters - /// The cancellation token. - /// A task that represents the asynchronous process operation. - Task ProcessQueryAsync( - BaseQueryParameters parameters, - CancellationToken cancellationToken = default); - - /// - /// Asynchronously process the change state of a UPS-RS workitem - /// - /// The to process. - /// The Work Item InstanceUID tag. - /// The cancellation token. - /// A task that represents the asynchronous process operation. - Task ProcessChangeStateAsync( - DicomDataset dataset, - string workitemInstanceUid, - CancellationToken cancellationToken = default); - - /// - /// Asynchronously update the workitem dataset. - /// - /// - /// If is not SCHEDULED and the is not present or does not match the one associated with , update will not go through. - /// - /// The to update. - /// The workitem instance UID. - /// The transaction UID associated with . - /// The cancellation token. - /// A task that represents the asynchronous update operation. - Task ProcessUpdateAsync( - DicomDataset dataset, - string workitemInstanceUid, - string transactionUid, - CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Workitem/IWorkItemStore.cs b/src/Microsoft.Health.Dicom.Core/Features/Workitem/IWorkItemStore.cs deleted file mode 100644 index 9e6624f66c..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Workitem/IWorkItemStore.cs +++ /dev/null @@ -1,46 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using FellowOakDicom; - -namespace Microsoft.Health.Dicom.Core.Features.Workitem; - -/// -/// Provides functionality to manage UPS-RS workitems. -/// -public interface IWorkitemStore -{ - /// - /// Asynchronously adds a workitem instance. - /// - /// The workitem instance identifier. - /// The dicom dataset - /// - /// The Proposed Watermark for the workitem - /// Defaults to identifier.Watermark when the Proposed Watermark is not set. - /// - /// The cancellation token. - /// A task that represents the asynchronous add operation. - Task AddWorkitemAsync(WorkitemInstanceIdentifier identifier, DicomDataset dataset, long? proposedWatermark = default, CancellationToken cancellationToken = default); - - /// - /// Asynchronously gets a workitem instance. - /// - /// The workitem instance identifier. - /// The cancellation token. - /// A task that represents the asynchronous get operation. - Task GetWorkitemAsync(WorkitemInstanceIdentifier identifier, CancellationToken cancellationToken = default); - - /// - /// Asynchronously deletes a workitem blob instance - /// - /// The workitem instance identifier. - /// The Proposed Watermark for the Workitem - /// The cancellation token. - /// - Task DeleteWorkitemAsync(WorkitemInstanceIdentifier identifier, long? proposedWatermark = default, CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Workitem/IWorkitemDatasetValidator.cs b/src/Microsoft.Health.Dicom.Core/Features/Workitem/IWorkitemDatasetValidator.cs deleted file mode 100644 index 6627320cb2..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Workitem/IWorkitemDatasetValidator.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Features.Store; - -namespace Microsoft.Health.Dicom.Core.Features.Workitem; - -/// -/// Peforms validation on incoming dataset that will be added as a workitem -/// -public interface IWorkitemDatasetValidator -{ - string Name { get; } - - /// - /// Validates the . - /// - /// The DICOM dataset to validate. - /// Thrown when the validation fails. - void Validate(DicomDataset dataset); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Workitem/IWorkitemOrchestrator.cs b/src/Microsoft.Health.Dicom.Core/Features/Workitem/IWorkitemOrchestrator.cs deleted file mode 100644 index 060eb3701c..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Workitem/IWorkitemOrchestrator.cs +++ /dev/null @@ -1,80 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Features.Query; -using Microsoft.Health.Dicom.Core.Features.Workitem.Model; -using Microsoft.Health.Dicom.Core.Messages.Workitem; - -namespace Microsoft.Health.Dicom.Core.Features.Workitem; - -/// -/// Provides functionality to orchestrate the adding of a UPS-RS workitem. -/// -public interface IWorkitemOrchestrator -{ - /// - /// Gets Workitem metadata from the store - /// - /// The workitem instance UID - /// The cancellation token. - /// - Task GetWorkitemMetadataAsync( - string workitemUid, - CancellationToken cancellationToken = default); - - /// - /// Asynchronously orchestrate the adding of a UPS-RS workitem. - /// - /// The workitem dataset to add. - /// The cancellation token. - /// A task that represents the asynchronous orchestration of the adding operation. - Task AddWorkitemAsync(DicomDataset dataset, CancellationToken cancellationToken); - - - /// - /// Asynchronously orchestrate updating the state of a UPS-RS workitem. - /// - /// The workitem dataset with the cancel request. - /// The workitem metadata - /// The target procedure step state - /// The cancellation token. - /// - Task UpdateWorkitemStateAsync(DicomDataset dataset, WorkitemMetadataStoreEntry workitemMetadata, ProcedureStepState targetProcedureStepState, CancellationToken cancellationToken); - - /// - /// Asynchronously orchestrate the searching of a UPS-RS workitem - /// - /// The query parameters - /// The cancellation token - /// - Task QueryAsync(BaseQueryParameters parameters, CancellationToken cancellationToken = default); - - /// - /// Asynchronously orchestrate the retrieval of a UPS-RS workitem - /// - /// The workitem instance identifier - /// The cancellation token - /// A task that represents the asynchronous orchestration of the retrieving a workitem DICOM dataset. - Task RetrieveWorkitemAsync(WorkitemInstanceIdentifier workitemInstanceIdentifier, CancellationToken cancellationToken = default); - - /// - /// Asynchronously orchestrate updating the UPS-RS workitem. - /// - /// The workitem dataset with the cancel request. - /// The workitem metadata. - /// The cancellation token. - Task UpdateWorkitemAsync(DicomDataset dataset, WorkitemMetadataStoreEntry workitemMetadata, CancellationToken cancellationToken); - - /// - /// Gets DicomDataset Blob from the Store for the given Workitem Instance identifier - /// - /// The workitem instance identifier - /// The cancellation token - /// A task that retrieves a workitem DICOM dataset from the Blob storage. - Task GetWorkitemBlobAsync(WorkitemInstanceIdentifier identifier, CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Workitem/IWorkitemQueryTagService.cs b/src/Microsoft.Health.Dicom.Core/Features/Workitem/IWorkitemQueryTagService.cs deleted file mode 100644 index bd437ab95e..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Workitem/IWorkitemQueryTagService.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -namespace Microsoft.Health.Dicom.Core.Features.Workitem; - -/// -/// Service provides queryable dicom tags. -/// -public interface IWorkitemQueryTagService -{ - /// - /// Get queryable dicom tags. - /// - /// The cancellation token. - /// Queryable dicom tags. - Task> GetQueryTagsAsync(CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Workitem/IWorkitemResponseBuilder.cs b/src/Microsoft.Health.Dicom.Core/Features/Workitem/IWorkitemResponseBuilder.cs deleted file mode 100644 index f860ba55bd..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Workitem/IWorkitemResponseBuilder.cs +++ /dev/null @@ -1,66 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Messages.Workitem; - -namespace Microsoft.Health.Dicom.Core.Features.Workitem; - -/// -/// Provides functionality to build the response for the add workitem transaction. -/// -public interface IWorkitemResponseBuilder -{ - /// - /// Builds the response. - /// - /// An instance of representing the response. - AddWorkitemResponse BuildAddResponse(); - - /// - /// Builds the response for cancel workitem. - /// - /// An instance of representing the response. - CancelWorkitemResponse BuildCancelResponse(); - - /// - /// Builds the response for change workitem state. - /// - /// An instance of representing the response. - ChangeWorkitemStateResponse BuildChangeWorkitemStateResponse(); - - /// - /// Builds the response for retrieve workitem - /// - /// An instance of representing the response. - RetrieveWorkitemResponse BuildRetrieveWorkitemResponse(); - - /// - /// Builds the response for update workitem. - /// - /// Workitem Instance UID. - /// An instance of representing the response. - UpdateWorkitemResponse BuildUpdateWorkitemResponse(string workitemInstanceUid = null); - - /// - /// Adds a successful entry to the response. - /// - /// The DICOM dataset that was successfully stored. - void AddSuccess(DicomDataset dicomDataset); - - /// - /// Adds a successful entry to the response with a status message - /// - /// The warning message related to the status - void AddSuccess(string warning = default); - - /// - /// Adds a failed entry to the response. - /// - /// The failure reason code. - /// The message related to the failure - /// The DICOM dataset that failed to be stored. - void AddFailure(ushort? failureReasonCode, string message = null, DicomDataset dicomDataset = null); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Workitem/Model/FinalStateRequirementCode.cs b/src/Microsoft.Health.Dicom.Core/Features/Workitem/Model/FinalStateRequirementCode.cs deleted file mode 100644 index 6013474a46..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Workitem/Model/FinalStateRequirementCode.cs +++ /dev/null @@ -1,38 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Features.Workitem.Model; - -/// -/// CC.2.5.1.1 UPS Final State Requirements -/// Table CC.2.5-1. Final State Codes -/// -public enum FinalStateRequirementCode -{ - /// - /// The UPS State may be set to either COMPLETED or CANCELED if this Attribute does not have a value. - /// - O, - - /// - /// The UPS State shall not be set to COMPLETED or CANCELED if this Attribute does not have a value. - /// - R, - - /// - /// The UPS State shall not be set to COMPLETED or CANCELED if the condition is met and this Attribute does not have a value. - /// - RC, - - /// - /// The UPS State shall not be set to COMPLETED if this Attribute does not have a value, but may be set to CANCELED. - /// - P, - - /// - /// The UPS State shall not be set to CANCELED if this Attribute does not have a value, but may be set to COMPLETED. - /// - X, -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Workitem/Model/FinalStateRequirementDetail.cs b/src/Microsoft.Health.Dicom.Core/Features/Workitem/Model/FinalStateRequirementDetail.cs deleted file mode 100644 index 19114db325..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Workitem/Model/FinalStateRequirementDetail.cs +++ /dev/null @@ -1,25 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using FellowOakDicom; - -namespace Microsoft.Health.Dicom.Core.Features.Workitem.Model; - -public sealed class FinalStateRequirementDetail -{ - public FinalStateRequirementDetail(DicomTag dicomTag, FinalStateRequirementCode requirementCode, HashSet sequenceRequirements = default) - { - DicomTag = dicomTag; - RequirementCode = requirementCode; - SequenceRequirements = sequenceRequirements; - } - - public DicomTag DicomTag { get; } - - public FinalStateRequirementCode RequirementCode { get; } - - public IReadOnlyCollection SequenceRequirements { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Workitem/Model/ProcedureStepState.cs b/src/Microsoft.Health.Dicom.Core/Features/Workitem/Model/ProcedureStepState.cs deleted file mode 100644 index 1ca9b097eb..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Workitem/Model/ProcedureStepState.cs +++ /dev/null @@ -1,55 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.ComponentModel.DataAnnotations; - -namespace Microsoft.Health.Dicom.Core.Features.Workitem; - -/// -/// Representing the Procedure Step State. -/// -public enum ProcedureStepState -{ - /// - /// Empty Procedure Step State. - /// - [Display(Name = ProcedureStepStateConstants.None)] - None, - - /// - /// The UPS is scheduled to be performed. - /// - [Display(Name = ProcedureStepStateConstants.Scheduled)] - Scheduled, - - /// - /// The UPS has been claimed and a Locking UID has been set. Performance of the UPS has likely started. - /// - [Display(Name = ProcedureStepStateConstants.InProgress)] - InProgress, - - /// - /// The UPS has been completed. - /// - [Display(Name = ProcedureStepStateConstants.Completed)] - Completed, - - /// - /// The UPS has been permanently stopped before or during performance of the step. - /// This may be due to voluntary or involuntary action by a human or machine. - /// Any further UPS-driven work required to complete the scheduled task must be performed by scheduling another (different) UPS. - /// - [Display(Name = ProcedureStepStateConstants.Canceled)] - Canceled -} - -internal static class ProcedureStepStateConstants -{ - internal const string None = @""; - internal const string Scheduled = @"SCHEDULED"; - internal const string InProgress = @"IN PROGRESS"; - internal const string Canceled = @"CANCELED"; - internal const string Completed = @"COMPLETED"; -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Workitem/Model/RequirementDetail.cs b/src/Microsoft.Health.Dicom.Core/Features/Workitem/Model/RequirementDetail.cs deleted file mode 100644 index 6541d0091e..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Workitem/Model/RequirementDetail.cs +++ /dev/null @@ -1,26 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Models; - -namespace Microsoft.Health.Dicom.Core.Features.Workitem.Model; - -public sealed class RequirementDetail -{ - public RequirementDetail(DicomTag dicomTag, RequirementCode requirementCode, IReadOnlyCollection sequenceRequirements = default) - { - DicomTag = dicomTag; - RequirementCode = requirementCode; - SequenceRequirements = sequenceRequirements; - } - - public DicomTag DicomTag { get; } - - public RequirementCode RequirementCode { get; } - - public IReadOnlyCollection SequenceRequirements { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Workitem/Model/WorkitemActionEvent.cs b/src/Microsoft.Health.Dicom.Core/Features/Workitem/Model/WorkitemActionEvent.cs deleted file mode 100644 index ace8a88b2d..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Workitem/Model/WorkitemActionEvent.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Features.Workitem; - -/// -/// Workitem action event -/// -public enum WorkitemActionEvent -{ - NCreate, - NActionToInProgress, - NActionToScheduled, - NActionToCompleted, - NActionToRequestCancel, - NActionToCanceled -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Workitem/Model/WorkitemInstanceIdentifier.cs b/src/Microsoft.Health.Dicom.Core/Features/Workitem/Model/WorkitemInstanceIdentifier.cs deleted file mode 100644 index 6dc0da4b7e..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Workitem/Model/WorkitemInstanceIdentifier.cs +++ /dev/null @@ -1,56 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Globalization; -using EnsureThat; - -namespace Microsoft.Health.Dicom.Core.Features.Workitem; - -public class WorkitemInstanceIdentifier -{ - private const StringComparison EqualsStringComparison = StringComparison.Ordinal; - - public WorkitemInstanceIdentifier( - string workitemUid, - long workitemKey, - int partitionKey = default, - long watermark = default) - { - EnsureArg.IsNotNullOrWhiteSpace(workitemUid, nameof(workitemUid)); - - WorkitemUid = workitemUid; - WorkitemKey = workitemKey; - PartitionKey = partitionKey; - Watermark = watermark; - } - - public int PartitionKey { get; } - - public long WorkitemKey { get; } - - public string WorkitemUid { get; } - - public long Watermark { get; } - - public override bool Equals(object obj) - { - if (obj is WorkitemInstanceIdentifier identifier) - { - return WorkitemUid.Equals(identifier.WorkitemUid, EqualsStringComparison) && - WorkitemKey == identifier.WorkitemKey && - PartitionKey == identifier.PartitionKey && - Watermark == identifier.Watermark; - } - - return false; - } - - public override int GetHashCode() - => (PartitionKey + WorkitemUid + WorkitemKey.ToString(CultureInfo.InvariantCulture) + Watermark.ToString(CultureInfo.InvariantCulture)).GetHashCode(EqualsStringComparison); - - public override string ToString() - => $"PartitionKey: {PartitionKey}, WorkitemKey: {WorkitemKey}, Watermark: {Watermark}"; -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Workitem/Model/WorkitemMetadataStoreEntry.cs b/src/Microsoft.Health.Dicom.Core/Features/Workitem/Model/WorkitemMetadataStoreEntry.cs deleted file mode 100644 index d3ccb545c6..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Workitem/Model/WorkitemMetadataStoreEntry.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Features.Workitem.Model; - -public sealed class WorkitemMetadataStoreEntry : WorkitemInstanceIdentifier -{ - public WorkitemMetadataStoreEntry(string workitemUid, long workitemKey, long watermark, int partitionKey = default) - : base(workitemUid, workitemKey, partitionKey, watermark) - { - } - - public WorkitemStoreStatus Status { get; set; } - - public string TransactionUid { get; set; } - - public string ProcedureStepStateStringValue => ProcedureStepState.GetStringValue(); - - public ProcedureStepState ProcedureStepState { get; set; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Workitem/Model/WorkitemQueryResult.cs b/src/Microsoft.Health.Dicom.Core/Features/Workitem/Model/WorkitemQueryResult.cs deleted file mode 100644 index eba0031fb2..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Workitem/Model/WorkitemQueryResult.cs +++ /dev/null @@ -1,20 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using EnsureThat; - -namespace Microsoft.Health.Dicom.Core.Features.Workitem; - -public class WorkitemQueryResult -{ - public WorkitemQueryResult(IEnumerable entries) - { - EnsureArg.IsNotNull(entries, nameof(entries)); - WorkitemInstances = entries; - } - - public IEnumerable WorkitemInstances { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Workitem/Model/WorkitemQueryTagStoreEntry.cs b/src/Microsoft.Health.Dicom.Core/Features/Workitem/Model/WorkitemQueryTagStoreEntry.cs deleted file mode 100644 index 297ab5eee4..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Workitem/Model/WorkitemQueryTagStoreEntry.cs +++ /dev/null @@ -1,35 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.ObjectModel; -using EnsureThat; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -namespace Microsoft.Health.Dicom.Core.Features.Workitem; - -/// -/// Represent each workitem query tag entry has retrieved from the store. -/// -public class WorkitemQueryTagStoreEntry : QueryTagEntry -{ - public WorkitemQueryTagStoreEntry(int key, string path, string vr) - { - Key = key; - Path = EnsureArg.IsNotNullOrWhiteSpace(path); - VR = EnsureArg.IsNotNullOrWhiteSpace(vr); - } - - /// - /// Key of this extended query tag entry. - /// - public int Key { get; } - - /// - /// Get the DicomTags that is the representation of the path for this tag - /// This is populated while fetching query tag from the db - /// - public ReadOnlyCollection PathTags { get; set; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Workitem/Model/WorkitemRequestType.cs b/src/Microsoft.Health.Dicom.Core/Features/Workitem/Model/WorkitemRequestType.cs deleted file mode 100644 index 4d3c92bf3b..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Workitem/Model/WorkitemRequestType.cs +++ /dev/null @@ -1,15 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Features.Workitem; - -/// -/// Workitem request type -/// -public enum WorkitemRequestType -{ - Add, - Update -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Workitem/Model/WorkitemStateTransitionResult.cs b/src/Microsoft.Health.Dicom.Core/Features/Workitem/Model/WorkitemStateTransitionResult.cs deleted file mode 100644 index 640bd49fce..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Workitem/Model/WorkitemStateTransitionResult.cs +++ /dev/null @@ -1,27 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Features.Workitem; - -/// -/// Workitem state transition result -/// -public class WorkitemStateTransitionResult -{ - public WorkitemStateTransitionResult(ProcedureStepState state, string code, bool isError) - { - State = state; - Code = code; - IsError = isError; - } - - public ProcedureStepState State { get; } - - public string Code { get; } - - public bool IsError { get; } - - public bool HasWarningWithCode => !IsError && !string.IsNullOrWhiteSpace(Code); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Workitem/Model/WorkitemStoreStatus.cs b/src/Microsoft.Health.Dicom.Core/Features/Workitem/Model/WorkitemStoreStatus.cs deleted file mode 100644 index 99a48c22bc..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Workitem/Model/WorkitemStoreStatus.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.Health.Dicom.Core.Features.Workitem.Model; - -/// -/// -/// -[SuppressMessage("Design", "CA1028:Enum Storage should be Int32", Justification = "Value is stored in SQL as TINYINT.")] -public enum WorkitemStoreStatus : byte -{ - /// Workitem being created - None = 0, - - /// Workitem created or updated - ReadWrite = 1, - - /// Workitem being updated/deleted, etc. - Read = 2 -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Workitem/ProcedureStepStateExtensions.cs b/src/Microsoft.Health.Dicom.Core/Features/Workitem/ProcedureStepStateExtensions.cs deleted file mode 100644 index 45065b72f3..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Workitem/ProcedureStepStateExtensions.cs +++ /dev/null @@ -1,182 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Globalization; -using EnsureThat; -using FellowOakDicom; - -namespace Microsoft.Health.Dicom.Core.Features.Workitem; - -/// -/// The Procedure Step State Extension (Helper methods) -/// -public static class ProcedureStepStateExtensions -{ - private const string Error0111 = "0111"; - private const string WarningB304 = "B304"; - private const string WarningB306 = "B306"; - private const string ErrorC300 = "C300"; - private const string ErrorC301 = "C301"; - private const string ErrorC302 = "C302"; - private const string ErrorC303 = "C303"; - private const string ErrorC307 = "C307"; - private const string ErrorC310 = "C310"; - private const string ErrorC311 = "C311"; - private const string ErrorC312 = "C312"; - - /// - /// Gets the Transition State from Procedure Step State for the given Action - /// - /// The Procedure Step State - /// The Action being performed on a Workitem - /// - /// - public static WorkitemStateTransitionResult GetTransitionState(this ProcedureStepState state, WorkitemActionEvent action, bool hasMatchingTransactionUid = true) - { - return state.CheckProcedureStepStateTransitionTable(action, hasMatchingTransactionUid); - } - - /// - /// Gets the display Name of a Procedure Step State - /// - /// The Procedure Step state - /// Returns the display name of the state - public static string GetStringValue(this ProcedureStepState state) - { - return state switch - { - ProcedureStepState.None => ProcedureStepStateConstants.None, - ProcedureStepState.Scheduled => ProcedureStepStateConstants.Scheduled, - ProcedureStepState.InProgress => ProcedureStepStateConstants.InProgress, - ProcedureStepState.Canceled => ProcedureStepStateConstants.Canceled, - ProcedureStepState.Completed => ProcedureStepStateConstants.Completed, - _ => throw new ArgumentOutOfRangeException(state.ToString()), - }; - } - - /// - /// Gets Procedure Step State from String - /// - /// The Procedure Step State as String - /// - public static ProcedureStepState GetProcedureStepState(string procedureStepStateStringValue) - { - if (string.IsNullOrWhiteSpace(procedureStepStateStringValue)) - { - return ProcedureStepState.None; - } - - foreach (var procedureStepState in Enum.GetValues()) - { - var displayName = procedureStepState.GetStringValue(); - if (string.Equals(displayName, procedureStepStateStringValue, StringComparison.OrdinalIgnoreCase)) - { - return procedureStepState; - } - } - - return ProcedureStepState.None; - } - - /// - /// Gets the procedure step state from the DicomDataset - /// - /// The DICOM dataset - /// Returns Procedure Step State - public static ProcedureStepState GetProcedureStepState(this DicomDataset dataset) - { - EnsureArg.IsNotNull(dataset, nameof(dataset)); - - if (!dataset.TryGetString(DicomTag.ProcedureStepState, out var stringValue)) - { - return ProcedureStepState.None; - } - - return GetProcedureStepState(stringValue); - } - - /// - /// The method returns the valid transitiion according to the spec - /// https://dicom.nema.org/dicom/2013/output/chtml/part04/chapter_CC.html#table_CC.1.1-2 - /// - /// The Workitem's Current Procedure Step State - /// The target event/action type - /// - /// - /// - private static WorkitemStateTransitionResult CheckProcedureStepStateTransitionTable(this ProcedureStepState state, WorkitemActionEvent action, bool hasMatchingTransactionUid = true) => (action, state) switch - { - (WorkitemActionEvent.NCreate, ProcedureStepState.None) => new WorkitemStateTransitionResult(ProcedureStepState.Scheduled, null, false), - (WorkitemActionEvent.NCreate, ProcedureStepState.Scheduled) => new WorkitemStateTransitionResult(ProcedureStepState.None, Error0111, true), - (WorkitemActionEvent.NCreate, ProcedureStepState.InProgress) => new WorkitemStateTransitionResult(ProcedureStepState.None, Error0111, true), - (WorkitemActionEvent.NCreate, ProcedureStepState.Completed) => new WorkitemStateTransitionResult(ProcedureStepState.None, Error0111, true), - (WorkitemActionEvent.NCreate, ProcedureStepState.Canceled) => new WorkitemStateTransitionResult(ProcedureStepState.None, Error0111, true), - - (WorkitemActionEvent.NActionToInProgress, ProcedureStepState.None) => new WorkitemStateTransitionResult(ProcedureStepState.None, ErrorC307, true), - (WorkitemActionEvent.NActionToInProgress, ProcedureStepState.Scheduled) => hasMatchingTransactionUid - ? new WorkitemStateTransitionResult(ProcedureStepState.InProgress, null, false) - : new WorkitemStateTransitionResult(ProcedureStepState.None, ErrorC301, true), - (WorkitemActionEvent.NActionToInProgress, ProcedureStepState.InProgress) => hasMatchingTransactionUid - ? new WorkitemStateTransitionResult(ProcedureStepState.None, ErrorC302, true) - : new WorkitemStateTransitionResult(ProcedureStepState.None, ErrorC301, true), - (WorkitemActionEvent.NActionToInProgress, ProcedureStepState.Completed) => hasMatchingTransactionUid - ? new WorkitemStateTransitionResult(ProcedureStepState.None, ErrorC300, true) - : new WorkitemStateTransitionResult(ProcedureStepState.None, ErrorC301, true), - (WorkitemActionEvent.NActionToInProgress, ProcedureStepState.Canceled) => hasMatchingTransactionUid - ? new WorkitemStateTransitionResult(ProcedureStepState.None, ErrorC300, true) - : new WorkitemStateTransitionResult(ProcedureStepState.None, ErrorC301, true), - - (WorkitemActionEvent.NActionToScheduled, ProcedureStepState.None) => new WorkitemStateTransitionResult(ProcedureStepState.None, ErrorC307, true), - (WorkitemActionEvent.NActionToScheduled, ProcedureStepState.Scheduled) => new WorkitemStateTransitionResult(ProcedureStepState.None, ErrorC303, true), - (WorkitemActionEvent.NActionToScheduled, ProcedureStepState.InProgress) => new WorkitemStateTransitionResult(ProcedureStepState.None, ErrorC303, true), - (WorkitemActionEvent.NActionToScheduled, ProcedureStepState.Completed) => new WorkitemStateTransitionResult(ProcedureStepState.None, ErrorC303, true), - (WorkitemActionEvent.NActionToScheduled, ProcedureStepState.Canceled) => new WorkitemStateTransitionResult(ProcedureStepState.None, ErrorC303, true), - - (WorkitemActionEvent.NActionToCompleted, ProcedureStepState.None) => new WorkitemStateTransitionResult(ProcedureStepState.None, ErrorC307, true), - (WorkitemActionEvent.NActionToCompleted, ProcedureStepState.Scheduled) => hasMatchingTransactionUid - ? new WorkitemStateTransitionResult(ProcedureStepState.None, ErrorC310, true) - : new WorkitemStateTransitionResult(ProcedureStepState.None, ErrorC301, true), - (WorkitemActionEvent.NActionToCompleted, ProcedureStepState.InProgress) => hasMatchingTransactionUid - ? new WorkitemStateTransitionResult(ProcedureStepState.Completed, null, false) - : new WorkitemStateTransitionResult(ProcedureStepState.None, ErrorC301, true), - (WorkitemActionEvent.NActionToCompleted, ProcedureStepState.Completed) => hasMatchingTransactionUid - ? new WorkitemStateTransitionResult(ProcedureStepState.None, WarningB306, false) - : new WorkitemStateTransitionResult(ProcedureStepState.None, ErrorC301, true), - (WorkitemActionEvent.NActionToCompleted, ProcedureStepState.Canceled) => hasMatchingTransactionUid - ? new WorkitemStateTransitionResult(ProcedureStepState.None, ErrorC300, true) - : new WorkitemStateTransitionResult(ProcedureStepState.None, ErrorC301, true), - - (WorkitemActionEvent.NActionToRequestCancel, ProcedureStepState.None) => new WorkitemStateTransitionResult(ProcedureStepState.None, ErrorC307, true), - (WorkitemActionEvent.NActionToRequestCancel, ProcedureStepState.Scheduled) => new WorkitemStateTransitionResult(ProcedureStepState.Canceled, null, false), - - // This case returns Error, with a message, because we do not support notifying the owner of the workitem instance about the cancellation request. - (WorkitemActionEvent.NActionToRequestCancel, ProcedureStepState.InProgress) => new WorkitemStateTransitionResult(ProcedureStepState.None, ErrorC312, true), - (WorkitemActionEvent.NActionToRequestCancel, ProcedureStepState.Completed) => new WorkitemStateTransitionResult(ProcedureStepState.None, ErrorC311, true), - (WorkitemActionEvent.NActionToRequestCancel, ProcedureStepState.Canceled) => new WorkitemStateTransitionResult(ProcedureStepState.None, WarningB304, false), - - (WorkitemActionEvent.NActionToCanceled, ProcedureStepState.None) => new WorkitemStateTransitionResult(ProcedureStepState.None, ErrorC307, true), - (WorkitemActionEvent.NActionToCanceled, ProcedureStepState.Scheduled) => hasMatchingTransactionUid - ? new WorkitemStateTransitionResult(ProcedureStepState.None, ErrorC310, true) - : new WorkitemStateTransitionResult(ProcedureStepState.None, ErrorC301, true), - (WorkitemActionEvent.NActionToCanceled, ProcedureStepState.InProgress) => hasMatchingTransactionUid - ? new WorkitemStateTransitionResult(ProcedureStepState.None, string.Empty, false) - : new WorkitemStateTransitionResult(ProcedureStepState.None, ErrorC301, true), - (WorkitemActionEvent.NActionToCanceled, ProcedureStepState.Completed) => hasMatchingTransactionUid - ? new WorkitemStateTransitionResult(ProcedureStepState.None, ErrorC300, true) - : new WorkitemStateTransitionResult(ProcedureStepState.None, ErrorC301, true), - (WorkitemActionEvent.NActionToCanceled, ProcedureStepState.Canceled) => hasMatchingTransactionUid - ? new WorkitemStateTransitionResult(ProcedureStepState.None, WarningB304, false) - : new WorkitemStateTransitionResult(ProcedureStepState.None, ErrorC301, true), - - _ => throw new Exceptions.NotSupportedException(string.Format( - CultureInfo.InvariantCulture, - DicomCoreResource.InvalidProcedureStepStateTransition, - string.Empty, - state, - string.Empty, - string.Empty)) - }; -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Workitem/QueryWorkitemHandler.cs b/src/Microsoft.Health.Dicom.Core/Features/Workitem/QueryWorkitemHandler.cs deleted file mode 100644 index 0159110109..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Workitem/QueryWorkitemHandler.cs +++ /dev/null @@ -1,39 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using MediatR; -using Microsoft.Health.Core.Features.Security.Authorization; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Security; -using Microsoft.Health.Dicom.Core.Messages.Workitem; - -namespace Microsoft.Health.Dicom.Core.Features.Workitem; - -public class QueryWorkitemHandler : BaseHandler, IRequestHandler -{ - private readonly IWorkitemService _workItemService; - - public QueryWorkitemHandler(IAuthorizationService authorizationService, IWorkitemService workItemService) - : base(authorizationService) - { - _workItemService = EnsureArg.IsNotNull(workItemService, nameof(workItemService)); - } - - public async Task Handle(QueryWorkitemResourceRequest request, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(request, nameof(request)); - - if (await AuthorizationService.CheckAccess(DataActions.Read, cancellationToken) != DataActions.Read) - { - throw new UnauthorizedDicomActionException(DataActions.Read); - } - - return await _workItemService.ProcessQueryAsync(request.Parameters, cancellationToken); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Workitem/RetrieveWorkitemRequestHandler.cs b/src/Microsoft.Health.Dicom.Core/Features/Workitem/RetrieveWorkitemRequestHandler.cs deleted file mode 100644 index f2d2a55d95..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Workitem/RetrieveWorkitemRequestHandler.cs +++ /dev/null @@ -1,43 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using MediatR; -using Microsoft.Health.Core.Features.Security.Authorization; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Security; -using Microsoft.Health.Dicom.Core.Messages.Workitem; - -namespace Microsoft.Health.Dicom.Core.Features.Workitem; - -public sealed class RetrieveWorkitemRequestHandler : BaseHandler, IRequestHandler -{ - private readonly IWorkitemService _workItemService; - - public RetrieveWorkitemRequestHandler(IAuthorizationService authorizationService, IWorkitemService workItemService) - : base(authorizationService) - { - _workItemService = EnsureArg.IsNotNull(workItemService, nameof(workItemService)); - } - - public async Task Handle(RetrieveWorkitemRequest request, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(request, nameof(request)); - - if (await AuthorizationService.CheckAccess(DataActions.Read, cancellationToken) != DataActions.Read) - { - throw new UnauthorizedDicomActionException(DataActions.Read); - } - - request.Validate(); - - return await _workItemService - .ProcessRetrieveAsync(request.WorkitemInstanceUid, cancellationToken) - .ConfigureAwait(false); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Workitem/UpdateWorkitemRequestHandler.cs b/src/Microsoft.Health.Dicom.Core/Features/Workitem/UpdateWorkitemRequestHandler.cs deleted file mode 100644 index e6b51c86fa..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Workitem/UpdateWorkitemRequestHandler.cs +++ /dev/null @@ -1,52 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using MediatR; -using Microsoft.Health.Core.Features.Security.Authorization; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Security; -using Microsoft.Health.Dicom.Core.Messages.Workitem; - -namespace Microsoft.Health.Dicom.Core.Features.Workitem; - -public class UpdateWorkitemRequestHandler : BaseHandler, IRequestHandler -{ - private readonly IWorkitemService _workItemService; - - public UpdateWorkitemRequestHandler( - IAuthorizationService authorizationService, - IWorkitemService workItemService) - : base(authorizationService) - { - _workItemService = EnsureArg.IsNotNull(workItemService, nameof(workItemService)); - } - - /// - public async Task Handle( - UpdateWorkitemRequest request, - CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(request, nameof(request)); - - // Verify that the user has Write permissions. - if (await AuthorizationService.CheckAccess(DataActions.Write, cancellationToken).ConfigureAwait(false) != DataActions.Write) - { - throw new UnauthorizedDicomActionException(DataActions.Write); - } - - // Validate that the Workitem UID is not empty and is valid. - // Also validate that the request payload is not empty. - // If transaction UID is passed, make sure it is also valid. - request.Validate(); - - return await _workItemService - .ProcessUpdateAsync(request.DicomDataset, request.WorkitemInstanceUid, request.TransactionUid, cancellationToken) - .ConfigureAwait(false); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemDatasetValidator.Add.cs b/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemDatasetValidator.Add.cs deleted file mode 100644 index c4d1f39b44..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemDatasetValidator.Add.cs +++ /dev/null @@ -1,26 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using FellowOakDicom; - -namespace Microsoft.Health.Dicom.Core.Features.Workitem; - -/// -/// Provides functionality to validate a to make sure it meets the minimum requirement when Adding. -/// Dicom 3.4.5.4.2.1 -/// -public class AddWorkitemDatasetValidator : WorkitemDatasetValidator -{ - /// - /// Validate requirement codes for dicom tags based on the spec. - /// Reference: - /// - /// Dataset to be validated. - protected override void OnValidate(DicomDataset dataset) - { - // TODO: return all validation exceptions together - dataset.ValidateAllRequirements(WorkitemRequestType.Add); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemDatasetValidator.Cancel.cs b/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemDatasetValidator.Cancel.cs deleted file mode 100644 index b13366fa37..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemDatasetValidator.Cancel.cs +++ /dev/null @@ -1,82 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Globalization; -using EnsureThat; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Store; -using Microsoft.Health.Dicom.Core.Features.Workitem.Model; - -namespace Microsoft.Health.Dicom.Core.Features.Workitem; - -public sealed class CancelWorkitemDatasetValidator : WorkitemDatasetValidator -{ - protected override void OnValidate(DicomDataset dataset) - { - // Validate the final-state requirements - dataset.ValidateFinalStateRequirement(); - } - - /// - /// Validates Workitem state in the store and procedure step state transition validity. - /// - /// Throws when workitem-metadata is null. - /// Throws when the workitem-metadata status is not read-write. - /// Throws when the workitem-metadata transition state has error. - /// - /// - /// The Workitem Uid - /// The Workitem Metadata - /// The state transition result - public static void ValidateWorkitemState( - string workitemUid, - WorkitemMetadataStoreEntry workitemMetadata, - WorkitemStateTransitionResult stateTransitionResult) - { - EnsureArg.IsNotNull(stateTransitionResult, nameof(stateTransitionResult)); - - if (workitemMetadata == null) - { - throw new WorkitemNotFoundException(); - } - - if (workitemMetadata.Status != WorkitemStoreStatus.ReadWrite) - { - throw new DatasetValidationException( - FailureReasonCodes.UpsPerformerChoosesNotToCancel, - string.Format( - CultureInfo.InvariantCulture, - DicomCoreResource.InvalidProcedureStepStateTransition, - stateTransitionResult.State.GetStringValue(), - workitemMetadata.ProcedureStepState.GetStringValue())); - } - - if (stateTransitionResult.IsError) - { - throw new DatasetValidationException( - FailureReasonCodes.ValidationFailure, - string.Format( - CultureInfo.InvariantCulture, - DicomCoreResource.InvalidProcedureStepStateTransition, - ProcedureStepState.Canceled, - stateTransitionResult.Code)); - } - - if (workitemMetadata.ProcedureStepState == ProcedureStepState.Completed) - { - throw new DatasetValidationException( - FailureReasonCodes.UpsIsAlreadyCompleted, - DicomCoreResource.WorkitemIsAlreadyCompleted); - } - - if (workitemMetadata.ProcedureStepState == ProcedureStepState.Canceled) - { - throw new DatasetValidationException( - FailureReasonCodes.UpsIsAlreadyCanceled, - DicomCoreResource.WorkitemIsAlreadyCanceled); - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemDatasetValidator.ChangeState.cs b/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemDatasetValidator.ChangeState.cs deleted file mode 100644 index 3b3b9c001b..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemDatasetValidator.ChangeState.cs +++ /dev/null @@ -1,103 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Immutable; -using System.Globalization; -using EnsureThat; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Store; -using Microsoft.Health.Dicom.Core.Features.Workitem.Model; -using Microsoft.Health.Dicom.Core.Models; - -namespace Microsoft.Health.Dicom.Core.Features.Workitem; - -public sealed class ChangeWorkitemStateDatasetValidator : WorkitemDatasetValidator -{ - // The legal values correspond to the requested state transition. They are: "IN PROGRESS", "COMPLETED", or "CANCELED". - private static readonly ImmutableHashSet AllowedTargetStatesForWorkitemChangeState = ImmutableHashSet.Create( - ProcedureStepStateConstants.InProgress, - ProcedureStepStateConstants.Canceled, - ProcedureStepStateConstants.Completed); - - protected override void OnValidate(DicomDataset dataset) - { - // Check for missing Transaction UID - dataset.ValidateRequirement(DicomTag.TransactionUID, RequirementCode.OneOne); - - // Check for missing Procedure Step State - dataset.ValidateRequirement(DicomTag.ProcedureStepState, RequirementCode.OneOne); - - // Check for allowed procedure step state value - var targetProcedureStepStateStringValue = dataset.GetString(DicomTag.ProcedureStepState); - if (!AllowedTargetStatesForWorkitemChangeState.Contains(targetProcedureStepStateStringValue)) - { - throw new DatasetValidationException( - FailureReasonCodes.ValidationFailure, - string.Format( - CultureInfo.InvariantCulture, - DicomCoreResource.UnexpectedValue, - DicomTag.ProcedureStepState, - string.Join(@",", AllowedTargetStatesForWorkitemChangeState))); - } - } - - /// - /// Validates that the passed Transaction UID matches against the workitem if it already has a Transaction UID - /// Otherwise, treats it as a new Transaction UID. - /// - /// Throws when the workitem-metadata status is not read-write. - /// Throws when the workitem-metadata transition state has error. - /// - /// - /// The Change Workitem State request DICOM dataset - /// The Workitem Metadata - internal static WorkitemStateTransitionResult ValidateWorkitemState(DicomDataset requestDataset, WorkitemMetadataStoreEntry workitemMetadata) - { - EnsureArg.IsNotNull(requestDataset, nameof(requestDataset)); - EnsureArg.IsNotNull(workitemMetadata, nameof(workitemMetadata)); - - // Check for the transition state rule validity - var targetProcedureStepStateStringValue = requestDataset.GetString(DicomTag.ProcedureStepState); - var targetProcedureStepState = ProcedureStepStateExtensions.GetProcedureStepState(targetProcedureStepStateStringValue); - - // the request Transaction UID must match the current Transaction UID. - var transactionUid = requestDataset.GetString(DicomTag.TransactionUID); - var hasMatchingTransactionUid = string.Equals(workitemMetadata.TransactionUid, transactionUid, System.StringComparison.Ordinal); - var hasNewTransactionUid = - string.IsNullOrWhiteSpace(workitemMetadata.TransactionUid) && - targetProcedureStepState == ProcedureStepState.InProgress; - - WorkitemActionEvent actionEvent = WorkitemActionEvent.NActionToInProgress; - switch (targetProcedureStepState) - { - case ProcedureStepState.Canceled: - actionEvent = WorkitemActionEvent.NActionToCanceled; - break; - case ProcedureStepState.Completed: - actionEvent = WorkitemActionEvent.NActionToCompleted; - break; - } - - // Check the state transition validity - var calculatedTransitionState = ProcedureStepStateExtensions.GetTransitionState( - workitemMetadata.ProcedureStepState, - actionEvent, - hasNewTransactionUid || hasMatchingTransactionUid); - - if (calculatedTransitionState.IsError) - { - throw new DatasetValidationException( - FailureReasonCodes.ValidationFailure, - string.Format( - CultureInfo.InvariantCulture, - DicomCoreResource.InvalidProcedureStepStateTransition, - targetProcedureStepStateStringValue, - calculatedTransitionState.Code)); - } - - return calculatedTransitionState; - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemDatasetValidator.Update.cs b/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemDatasetValidator.Update.cs deleted file mode 100644 index 9402d78276..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemDatasetValidator.Update.cs +++ /dev/null @@ -1,88 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Store; -using Microsoft.Health.Dicom.Core.Features.Workitem.Model; - -namespace Microsoft.Health.Dicom.Core.Features.Workitem; - -/// -/// Provides functionality to validate a to make sure it meets the minimum requirement when Updating. -/// Dicom 5.4.2.1 -/// -public class UpdateWorkitemDatasetValidator : WorkitemDatasetValidator -{ - /// - /// Validate requirement codes for dicom tags based on the spec. - /// Reference: - /// - /// Dataset to be validated. - protected override void OnValidate(DicomDataset dataset) - { - dataset.ValidateAllRequirements(WorkitemRequestType.Update); - } - - /// - /// Validates Workitem state in the store and procedure step state transition validity. - /// Also validate that the passed Transaction Uid matches the existing transaction Uid. - /// - /// Throws when workitem-metadata is null. - /// Throws when the workitem-metadata status is not read-write. - /// Throws when the workitem-metadata procedure step state is not In Progress. - /// Throws when the transaction uid does not match the existing transaction uid. - /// - /// - /// The Transaction Uid. - /// The Workitem Metadata. - public static void ValidateWorkitemStateAndTransactionUid(string transactionUid, WorkitemMetadataStoreEntry workitemMetadata) - { - if (workitemMetadata == null) - { - throw new WorkitemNotFoundException(); - } - - switch (workitemMetadata.ProcedureStepState) - { - case ProcedureStepState.Scheduled: - // Update can be made when in Scheduled state. Transaction UID cannot be present though. - if (!string.IsNullOrWhiteSpace(transactionUid)) - { - throw new DatasetValidationException( - FailureReasonCodes.UpsTransactionUidIncorrect, - DicomCoreResource.InvalidTransactionUID); - } - break; - case ProcedureStepState.InProgress: - // Transaction UID must be provided - if (string.IsNullOrWhiteSpace(transactionUid)) - { - throw new DatasetValidationException( - FailureReasonCodes.UpsTransactionUidAbsent, - DicomCoreResource.InvalidWorkitemInstanceTargetUri); - } - - // Provided Transaction UID has to be equal to the existing Transaction UID. - if (!string.Equals(workitemMetadata.TransactionUid, transactionUid, System.StringComparison.Ordinal)) - { - throw new DatasetValidationException( - FailureReasonCodes.UpsTransactionUidIncorrect, - DicomCoreResource.InvalidWorkitemInstanceTargetUri); - } - - break; - case ProcedureStepState.Completed: - throw new DatasetValidationException( - FailureReasonCodes.UpsIsAlreadyCompleted, - string.Concat(DicomCoreResource.UpdateWorkitemInstanceConflictFailure, " ", DicomCoreResource.WorkitemIsAlreadyCompleted)); - - case ProcedureStepState.Canceled: - throw new DatasetValidationException( - FailureReasonCodes.UpsIsAlreadyCanceled, - string.Concat(DicomCoreResource.UpdateWorkitemInstanceConflictFailure, " ", DicomCoreResource.WorkitemIsAlreadyCanceled)); - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemDatasetValidator.cs b/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemDatasetValidator.cs deleted file mode 100644 index bdfe3e9409..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemDatasetValidator.cs +++ /dev/null @@ -1,23 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using FellowOakDicom; - -namespace Microsoft.Health.Dicom.Core.Features.Workitem; - -public abstract class WorkitemDatasetValidator : IWorkitemDatasetValidator -{ - public string Name => GetType().Name; - - public void Validate(DicomDataset dataset) - { - EnsureArg.IsNotNull(dataset, nameof(dataset)); - - OnValidate(dataset); - } - - protected abstract void OnValidate(DicomDataset dataset); -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemDatasetValidatorExtension.cs b/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemDatasetValidatorExtension.cs deleted file mode 100644 index 45e795f819..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemDatasetValidatorExtension.cs +++ /dev/null @@ -1,851 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Workitem.Model; -using Microsoft.Health.Dicom.Core.Models; - -namespace Microsoft.Health.Dicom.Core.Features.Workitem; - -/// -/// Workitem dataset validator extension -/// -internal static class WorkitemDatasetValidatorExtension -{ - private static readonly HashSet AddWorkitemRequirements = GetRequirements(WorkitemRequestType.Add); - private static readonly HashSet UpdateWorkitemRequirements = GetRequirements(WorkitemRequestType.Update); - - public static void ValidateAllRequirements(this DicomDataset dataset, WorkitemRequestType requestType) - { - HashSet requirements = null; - - switch (requestType) - { - case WorkitemRequestType.Add: - requirements = AddWorkitemRequirements; - break; - case WorkitemRequestType.Update: - requirements = UpdateWorkitemRequirements; - break; - } - - dataset.ValidateAllRequirements(requirements); - } - - /// - /// Refer - /// - /// Request type: Add or Update. - /// Set containing all the requirements for specified request type. - private static HashSet GetRequirements(WorkitemRequestType requestType) - { - HashSet requirements = null; - - switch (requestType) - { - case WorkitemRequestType.Add: - GetAddWorkitemRequirements(out requirements); - break; - case WorkitemRequestType.Update: - GetUpdateWorkitemRequirements(out requirements); - break; - } - - return requirements; - } - - /// - /// Get validation requirements for Add Workitem dataset. - /// Reference: https://dicom.nema.org/medical/dicom/current/output/html/part04.html#table_CC.2.5-3" - /// - /// Hashset containing requirements to be validated. - private static void GetAddWorkitemRequirements(out HashSet requirements) - { - requirements = new HashSet - { - new RequirementDetail(DicomTag.TransactionUID, RequirementCode.TwoTwo), - new RequirementDetail(DicomTag.TransactionUID, RequirementCode.MustBeEmpty), - }; - - requirements.UnionWith(GetSOPCommonModuleRequirements(WorkitemRequestType.Add)); - requirements.UnionWith(GetUnifiedProcedureStepScheduledProcedureInformationModuleRequirements(WorkitemRequestType.Add)); - requirements.UnionWith(GetUnifiedProcedureStepRelationshipModuleRequirements(WorkitemRequestType.Add)); - requirements.UnionWith(GetPatientDemographicModuleRequirements()); - requirements.UnionWith(GetPatientMedicalModuleRequirements()); - requirements.UnionWith(GetVisitIdentificationModuleRequirements()); - requirements.UnionWith(GetVisitStatusModuleRequirements()); - requirements.UnionWith(GetVisitAdmissionModuleRequirements()); - requirements.UnionWith(GetUnifiedProcedureStepProgressInformationModuleRequirements(WorkitemRequestType.Add)); - requirements.UnionWith(GetUnifiedProcedureStepPerformedProcedureInformationModuleRequirements(WorkitemRequestType.Add)); - } - - /// - /// Get validation requirements for Update Workitem dataset. - /// Reference: https://dicom.nema.org/medical/dicom/current/output/html/part04.html#table_CC.2.5-3" - /// - /// Hashset containing requirements to be validated. - private static void GetUpdateWorkitemRequirements(out HashSet requirements) - { - requirements = new HashSet(GetSOPCommonModuleRequirements(WorkitemRequestType.Update)); - requirements.UnionWith(GetUnifiedProcedureStepScheduledProcedureInformationModuleRequirements(WorkitemRequestType.Update)); - requirements.UnionWith(GetUnifiedProcedureStepRelationshipModuleRequirements(WorkitemRequestType.Update)); - requirements.UnionWith(GetPatientDemographicModuleRequirements()); - requirements.UnionWith(GetPatientMedicalModuleRequirements()); - requirements.UnionWith(GetVisitIdentificationModuleRequirements()); - requirements.UnionWith(GetVisitStatusModuleRequirements()); - requirements.UnionWith(GetVisitAdmissionModuleRequirements()); - requirements.UnionWith(GetUnifiedProcedureStepProgressInformationModuleRequirements(WorkitemRequestType.Update)); - requirements.UnionWith(GetUnifiedProcedureStepPerformedProcedureInformationModuleRequirements(WorkitemRequestType.Update)); - } - - private static HashSet GetSOPCommonModuleRequirements(WorkitemRequestType requestType) - { - HashSet requirements = new HashSet - { - new RequirementDetail(DicomTag.SpecificCharacterSet, RequirementCode.OneCOneC), - new RequirementDetail(DicomTag.SOPClassUID, requestType == WorkitemRequestType.Add ? RequirementCode.OneOne : RequirementCode.NotAllowed), - new RequirementDetail(DicomTag.SOPInstanceUID, requestType == WorkitemRequestType.Add ? RequirementCode.OneOne : RequirementCode.NotAllowed), - new RequirementDetail(DicomTag.InstanceCreationDate, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.InstanceCreationTime, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.InstanceCoercionDateTime, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.InstanceCreatorUID, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.RelatedGeneralSOPClassUID, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.OriginalSpecializedSOPClassUID, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.CodingSchemeIdentificationSequence, RequirementCode.ThreeThree, new HashSet - { - new RequirementDetail(DicomTag.CodingSchemeDesignator, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.CodingSchemeRegistry, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.CodingSchemeUID, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.CodingSchemeExternalID, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.CodingSchemeName, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.CodingSchemeVersion, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.CodingSchemeResponsibleOrganization, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.CodingSchemeResourcesSequence, RequirementCode.ThreeThree, new HashSet - { - new RequirementDetail(DicomTag.CodingSchemeURLType, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.CodingSchemeURL, RequirementCode.ThreeThree), - }), - }), - new RequirementDetail(DicomTag.ContextGroupIdentificationSequence, RequirementCode.ThreeThree, new HashSet - { - new RequirementDetail(DicomTag.ContextIdentifier, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.ContextUID, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.MappingResource, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.ContextGroupVersion, RequirementCode.ThreeThree), - }), - new RequirementDetail(DicomTag.MappingResourceIdentificationSequence, RequirementCode.ThreeThree, new HashSet - { - new RequirementDetail(DicomTag.MappingResource, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.MappingResourceUID, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.MappingResourceName, RequirementCode.ThreeThree), - }), - new RequirementDetail(DicomTag.TimezoneOffsetFromUTC, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.ContributingEquipmentSequence, RequirementCode.ThreeThree, new HashSet - { - new RequirementDetail(DicomTag.PurposeOfReferenceCodeSequence, RequirementCode.ThreeThree, GetCodeSequenceMacroAttributesRequirements()), - new RequirementDetail(DicomTag.Manufacturer, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.InstitutionName, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.InstitutionAddress, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.StationName, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.InstitutionalDepartmentName, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.InstitutionalDepartmentTypeCodeSequence, RequirementCode.ThreeThree, GetCodeSequenceMacroAttributesRequirements()), - new RequirementDetail(DicomTag.OperatorsName, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.OperatorIdentificationSequence, RequirementCode.ThreeThree, GetPersonIdentificationMacroAttributesRequirements()), - new RequirementDetail(DicomTag.ManufacturerModelName, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.DeviceSerialNumber, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.SoftwareVersions, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.DeviceUID, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.UDISequence, RequirementCode.ThreeThree, new HashSet - { - new RequirementDetail(DicomTag.UniqueDeviceIdentifier, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.DeviceDescription, RequirementCode.ThreeThree), - }), - new RequirementDetail(DicomTag.SpatialResolution, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.DateOfLastCalibration, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.TimeOfLastCalibration, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.ContributionDateTime, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.ContributionDescription, RequirementCode.ThreeThree), - }), - new RequirementDetail(DicomTag.InstanceNumber, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.SOPInstanceStatus, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.SOPAuthorizationDateTime, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.SOPAuthorizationComment, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.AuthorizationEquipmentCertificationNumber, RequirementCode.ThreeThree), - }; - - requirements.UnionWith(GetDigitalSignatureMacroAttributesRequirements()); - requirements.Add(new RequirementDetail(DicomTag.EncryptedAttributesSequence, RequirementCode.ThreeThree, new HashSet - { - new RequirementDetail(DicomTag.EncryptedContentTransferSyntaxUID, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.EncryptedContent, RequirementCode.ThreeThree), - })); - requirements.Add(GetOriginalAttributesMacroAttributesRequirements()); - requirements.Add(GetHL7StructuredDocumentReferenceSequenceRequirements()); - requirements.Add(new RequirementDetail(DicomTag.LongitudinalTemporalInformationModified, RequirementCode.ThreeThree)); - requirements.Add(new RequirementDetail(DicomTag.QueryRetrieveView, RequirementCode.ThreeThree)); - requirements.Add(GetConversionSourceAttributesSequenceRequirements()); - requirements.Add(new RequirementDetail(DicomTag.ContentQualification, RequirementCode.ThreeThree)); - requirements.Add(new RequirementDetail(DicomTag.PrivateDataElementCharacteristicsSequence, RequirementCode.ThreeThree, new HashSet - { - new RequirementDetail(DicomTag.PrivateGroupReference, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.PrivateCreatorReference, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.PrivateDataElementDefinitionSequence, RequirementCode.ThreeThree, new HashSet - { - new RequirementDetail(DicomTag.PrivateDataElement, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.PrivateDataElementValueMultiplicity, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.PrivateDataElementValueRepresentation, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.PrivateDataElementNumberOfItems, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.PrivateDataElementKeyword, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.PrivateDataElementName, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.PrivateDataElementDescription, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.PrivateDataElementEncoding, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.RetrieveURI, RequirementCode.ThreeThree), - }), - new RequirementDetail(DicomTag.BlockIdentifyingInformationStatus, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.NonidentifyingPrivateElements, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.DeidentificationActionSequence, RequirementCode.ThreeThree, new HashSet - { - new RequirementDetail(DicomTag.IdentifyingPrivateElements, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.DeidentificationAction, RequirementCode.ThreeThree), - }), - })); - requirements.Add(new RequirementDetail(DicomTag.InstanceOriginStatus, RequirementCode.ThreeThree)); - requirements.Add(new RequirementDetail(DicomTag.BarcodeValue, RequirementCode.ThreeThree)); - requirements.UnionWith(GetGeneralProcedureProtocolReferenceMacroAttributesRequirements()); - - return requirements; - } - - private static HashSet GetUnifiedProcedureStepScheduledProcedureInformationModuleRequirements(WorkitemRequestType requestType) - { - return new HashSet - { - new RequirementDetail(DicomTag.ScheduledProcedureStepPriority, requestType == WorkitemRequestType.Add ? RequirementCode.OneOne : RequirementCode.ThreeOne), - new RequirementDetail(DicomTag.ScheduledProcedureStepModificationDateTime, RequirementCode.OneOne), - new RequirementDetail(DicomTag.ProcedureStepLabel, requestType == WorkitemRequestType.Add ? RequirementCode.OneOne : RequirementCode.ThreeOne), - new RequirementDetail(DicomTag.WorklistLabel, requestType == WorkitemRequestType.Add ? RequirementCode.TwoOne : RequirementCode.ThreeOne), - new RequirementDetail(DicomTag.ScheduledProcessingParametersSequence, requestType == WorkitemRequestType.Add ? RequirementCode.TwoTwo : RequirementCode.ThreeTwo, GetUPSContentItemMacroRequirements()), - new RequirementDetail(DicomTag.ScheduledStationNameCodeSequence, requestType == WorkitemRequestType.Add ? RequirementCode.TwoTwo : RequirementCode.ThreeTwo, GetUPSCodeSequenceMacroRequirements()), - new RequirementDetail(DicomTag.ScheduledStationClassCodeSequence, requestType == WorkitemRequestType.Add ? RequirementCode.TwoTwo : RequirementCode.ThreeTwo, GetUPSCodeSequenceMacroRequirements()), - new RequirementDetail(DicomTag.ScheduledStationGeographicLocationCodeSequence, requestType == WorkitemRequestType.Add ? RequirementCode.TwoTwo : RequirementCode.ThreeTwo, GetUPSCodeSequenceMacroRequirements()), - new RequirementDetail(DicomTag.ScheduledHumanPerformersSequence, requestType == WorkitemRequestType.Add ? RequirementCode.TwoCTwoC : RequirementCode.ThreeTwo, new HashSet - { - new RequirementDetail(DicomTag.HumanPerformerCodeSequence, RequirementCode.OneOne, GetUPSCodeSequenceMacroRequirements()), - new RequirementDetail(DicomTag.HumanPerformerName, RequirementCode.OneOne), - new RequirementDetail(DicomTag.HumanPerformerOrganization, RequirementCode.OneOne), - }), - new RequirementDetail(DicomTag.ScheduledProcedureStepStartDateTime, requestType == WorkitemRequestType.Add ? RequirementCode.OneOne : RequirementCode.ThreeOne), - new RequirementDetail(DicomTag.ExpectedCompletionDateTime, RequirementCode.ThreeOne), - new RequirementDetail(DicomTag.ScheduledProcedureStepExpirationDateTime, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.ScheduledWorkitemCodeSequence, requestType == WorkitemRequestType.Add ? RequirementCode.TwoTwo : RequirementCode.ThreeOne, GetUPSCodeSequenceMacroRequirements()), - new RequirementDetail(DicomTag.CommentsOnTheScheduledProcedureStep, requestType == WorkitemRequestType.Add ? RequirementCode.TwoTwo : RequirementCode.ThreeOne), - new RequirementDetail(DicomTag.InputReadinessState, requestType == WorkitemRequestType.Add ? RequirementCode.OneOne : RequirementCode.ThreeOne), - new RequirementDetail(DicomTag.InputInformationSequence, requestType == WorkitemRequestType.Add ? RequirementCode.TwoTwo : RequirementCode.ThreeTwo, GetReferencedInstancesAndAccessMacroRequirements()), - new RequirementDetail(DicomTag.StudyInstanceUID, requestType == WorkitemRequestType.Add ? RequirementCode.OneCTwo : RequirementCode.ThreeTwo), - new RequirementDetail(DicomTag.OutputDestinationSequence, RequirementCode.ThreeThree, new HashSet - { - new RequirementDetail(DicomTag.ReferencedSOPClassUID, RequirementCode.OneCOne), - new RequirementDetail(DicomTag.DICOMStorageSequence, RequirementCode.OneCOne, new HashSet - { - new RequirementDetail(DicomTag.DestinationAE, RequirementCode.OneOne), - }), - new RequirementDetail(DicomTag.STOWRSStorageSequence, RequirementCode.OneCOne, new HashSet - { - new RequirementDetail(DicomTag.StorageURL, RequirementCode.OneOne), - }), - new RequirementDetail(DicomTag.XDSStorageSequence, RequirementCode.OneCOne, new HashSet - { - new RequirementDetail(DicomTag.RepositoryUniqueID, RequirementCode.OneOne), - new RequirementDetail(DicomTag.HomeCommunityID, RequirementCode.ThreeTwo), - }), - }), - }; - } - - private static HashSet GetUnifiedProcedureStepRelationshipModuleRequirements(WorkitemRequestType requestType) - { - HashSet requirements = new HashSet - { - new RequirementDetail(DicomTag.PatientName, requestType == WorkitemRequestType.Add ? RequirementCode.TwoTwo : RequirementCode.NotAllowed), - new RequirementDetail(DicomTag.PatientID, requestType == WorkitemRequestType.Add ? RequirementCode.OneCTwo : RequirementCode.NotAllowed), - }; - - requirements.UnionWith(GetIssuerOfPatientIDMacroRequirements(requestType)); - - requirements.Add(new RequirementDetail(DicomTag.OtherPatientIDsSequence, requestType == WorkitemRequestType.Add ? RequirementCode.TwoTwo : RequirementCode.ThreeThree, GetOtherPatientIDSequenceRequirements(requestType))); - requirements.Add(new RequirementDetail(DicomTag.PatientBirthDate, requestType == WorkitemRequestType.Add ? RequirementCode.TwoTwo : RequirementCode.NotAllowed)); - requirements.Add(new RequirementDetail(DicomTag.PatientSex, requestType == WorkitemRequestType.Add ? RequirementCode.TwoTwo : RequirementCode.NotAllowed)); - requirements.Add(new RequirementDetail(DicomTag.ReferencedPatientPhotoSequence, RequirementCode.ThreeThree, GetReferencedInstancesAndAccessMacroRequirements())); - requirements.Add(new RequirementDetail(DicomTag.AdmissionID, requestType == WorkitemRequestType.Add ? RequirementCode.TwoTwo : RequirementCode.NotAllowed)); - requirements.Add(new RequirementDetail(DicomTag.IssuerOfAdmissionIDSequence, requestType == WorkitemRequestType.Add ? RequirementCode.TwoTwo : RequirementCode.NotAllowed, GetHL7v2HierarchicDesignatorMacroForAddRequirements())); - requirements.Add(new RequirementDetail(DicomTag.AdmittingDiagnosesDescription, requestType == WorkitemRequestType.Add ? RequirementCode.TwoTwo : RequirementCode.NotAllowed)); - requirements.Add(new RequirementDetail(DicomTag.AdmittingDiagnosesCodeSequence, requestType == WorkitemRequestType.Add ? RequirementCode.TwoTwo : RequirementCode.NotAllowed, GetUPSCodeSequenceMacroRequirements())); - requirements.Add(new RequirementDetail(DicomTag.ReferencedRequestSequence, requestType == WorkitemRequestType.Add ? RequirementCode.TwoTwo : RequirementCode.NotAllowed, new HashSet - { - new RequirementDetail(DicomTag.StudyInstanceUID, RequirementCode.OneOne), - new RequirementDetail(DicomTag.AccessionNumber, RequirementCode.TwoTwo), - new RequirementDetail(DicomTag.IssuerOfAccessionNumberSequence, RequirementCode.TwoTwo, GetHL7v2HierarchicDesignatorMacroForAddRequirements()), - new RequirementDetail(DicomTag.PlacerOrderNumberImagingServiceRequest, RequirementCode.ThreeOne), - new RequirementDetail(DicomTag.OrderPlacerIdentifierSequence, RequirementCode.TwoTwo, GetHL7v2HierarchicDesignatorMacroForAddRequirements()), - new RequirementDetail(DicomTag.FillerOrderNumberImagingServiceRequest, RequirementCode.ThreeOne), - new RequirementDetail(DicomTag.OrderFillerIdentifierSequence, RequirementCode.TwoTwo, GetHL7v2HierarchicDesignatorMacroForAddRequirements()), - new RequirementDetail(DicomTag.RequestedProcedureID, RequirementCode.TwoTwo), - new RequirementDetail(DicomTag.RequestedProcedureDescription, RequirementCode.TwoTwo), - new RequirementDetail(DicomTag.RequestedProcedureCodeSequence, RequirementCode.TwoTwo, GetUPSCodeSequenceMacroRequirements()), - new RequirementDetail(DicomTag.ReasonForTheRequestedProcedure, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.ReasonForRequestedProcedureCodeSequence, RequirementCode.ThreeThree, GetUPSCodeSequenceMacroRequirements()), - new RequirementDetail(DicomTag.RequestedProcedureComments, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.ConfidentialityCode, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.NamesOfIntendedRecipientsOfResults, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.ImagingServiceRequestComments, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.RequestingPhysician, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.RequestingService, RequirementCode.ThreeOne), - new RequirementDetail(DicomTag.RequestingServiceCodeSequence, RequirementCode.ThreeThree, GetUPSCodeSequenceMacroRequirements()), - new RequirementDetail(DicomTag.IssueDateOfImagingServiceRequest, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.IssueTimeOfImagingServiceRequest, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.ReferringPhysicianName, RequirementCode.ThreeThree), - })); - requirements.Add(new RequirementDetail(DicomTag.ReplacedProcedureStepSequence, requestType == WorkitemRequestType.Add ? RequirementCode.OneCOneC : RequirementCode.NotAllowed, new HashSet - { - new RequirementDetail(DicomTag.ReferencedSOPClassUID, RequirementCode.OneOne), - new RequirementDetail(DicomTag.ReferencedSOPInstanceUID, RequirementCode.OneOne), - })); - requirements.Add(new RequirementDetail(DicomTag.TypeOfPatientID, RequirementCode.ThreeThree)); - requirements.Add(new RequirementDetail(DicomTag.PatientBirthDateInAlternativeCalendar, RequirementCode.ThreeThree)); - requirements.Add(new RequirementDetail(DicomTag.PatientDeathDateInAlternativeCalendar, RequirementCode.ThreeThree)); - requirements.Add(new RequirementDetail(DicomTag.PatientAlternativeCalendar, RequirementCode.ThreeThree)); - requirements.Add(new RequirementDetail(DicomTag.ReasonForVisit, RequirementCode.ThreeThree)); - requirements.Add(new RequirementDetail(DicomTag.ReasonForVisitCodeSequence, RequirementCode.ThreeThree, GetCodeSequenceMacroAttributesRequirements())); - - return requirements; - } - - /// - /// Reference: https://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.2.3 - /// - /// HashSet of requirements. - private static HashSet GetPatientDemographicModuleRequirements() - { - return new HashSet - { - new RequirementDetail(DicomTag.PatientAge, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.Occupation, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.ConfidentialityConstraintOnPatientDataDescription, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.PatientBirthDate, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.PatientBirthTime, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.PatientSex, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.QualityControlSubject, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.PatientInsurancePlanCodeSequence, RequirementCode.ThreeThree, GetCodeSequenceMacroAttributesRequirements()), - new RequirementDetail(DicomTag.PatientPrimaryLanguageCodeSequence, RequirementCode.ThreeThree, GetPrimaryLanguageCodeSequenceRequirements()), - new RequirementDetail(DicomTag.PatientSize, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.PatientWeight, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.PatientSizeCodeSequence, RequirementCode.ThreeThree, GetCodeSequenceMacroAttributesRequirements()), - new RequirementDetail(DicomTag.PatientAddress, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.MilitaryRank, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.BranchOfService, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.CountryOfResidence, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.RegionOfResidence, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.PatientTelephoneNumbers, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.PatientTelecomInformation, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.EthnicGroup, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.PatientReligiousPreference, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.PatientComments, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.ResponsiblePerson, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.ResponsiblePersonRole, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.ResponsibleOrganization, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.PatientSpeciesDescription, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.PatientSpeciesCodeSequence, RequirementCode.ThreeThree, GetCodeSequenceMacroAttributesRequirements()), - new RequirementDetail(DicomTag.PatientBreedDescription, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.PatientBreedCodeSequence, RequirementCode.ThreeThree, GetCodeSequenceMacroAttributesRequirements()), - new RequirementDetail(DicomTag.BreedRegistrationSequence, RequirementCode.ThreeThree, new HashSet - { - new RequirementDetail(DicomTag.BreedRegistrationNumber, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.BreedRegistryCodeSequence, RequirementCode.ThreeThree, GetCodeSequenceMacroAttributesRequirements()), - }), - new RequirementDetail(DicomTag.StrainDescription, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.StrainNomenclature, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.StrainCodeSequence, RequirementCode.ThreeThree, GetCodeSequenceMacroAttributesRequirements()), - new RequirementDetail(DicomTag.StrainAdditionalInformation, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.StrainStockSequence, RequirementCode.ThreeThree, new HashSet - { - new RequirementDetail(DicomTag.StrainStockNumber, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.StrainSource, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.StrainSourceRegistryCodeSequence, RequirementCode.ThreeThree, GetCodeSequenceMacroAttributesRequirements()), - }), - new RequirementDetail(DicomTag.GeneticModificationsSequence, RequirementCode.ThreeThree, new HashSet - { - new RequirementDetail(DicomTag.GeneticModificationsDescription, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.GeneticModificationsNomenclature, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.GeneticModificationsCodeSequence, RequirementCode.ThreeThree, GetCodeSequenceMacroAttributesRequirements()), - }), - }; - } - - /// - /// Reference: https://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.2.4 - /// - /// HashSet of requirements. - private static HashSet GetPatientMedicalModuleRequirements() - { - return new HashSet - { - new RequirementDetail(DicomTag.MedicalAlerts, RequirementCode.ThreeTwo), - new RequirementDetail(DicomTag.PregnancyStatus, RequirementCode.ThreeTwo), - new RequirementDetail(DicomTag.SpecialNeeds, RequirementCode.ThreeTwo), - new RequirementDetail(DicomTag.Allergies, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.SmokingStatus, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.AdditionalPatientHistory, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.LastMenstrualDate, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.PatientSexNeutered, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.PatientBodyMassIndex, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.MeasuredAPDimension, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.MeasuredLateralDimension, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.PatientState, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.PertinentDocumentsSequence, RequirementCode.ThreeThree, new HashSet - { - new RequirementDetail(DicomTag.ReferencedSOPClassUID, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.ReferencedSOPInstanceUID, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.PurposeOfReferenceCodeSequence, RequirementCode.ThreeThree, GetCodeSequenceMacroAttributesRequirements()), - new RequirementDetail(DicomTag.DocumentTitle, RequirementCode.ThreeThree), - }), - new RequirementDetail(DicomTag.PertinentResourcesSequence, RequirementCode.ThreeThree, new HashSet - { - new RequirementDetail(DicomTag.RetrieveURI, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.ResourceDescription, RequirementCode.ThreeThree), - }), - new RequirementDetail(DicomTag.PatientClinicalTrialParticipationSequence, RequirementCode.ThreeThree, new HashSet - { - new RequirementDetail(DicomTag.ClinicalTrialSponsorName, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.ClinicalTrialProtocolID, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.ClinicalTrialProtocolName, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.ClinicalTrialSiteID, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.ClinicalTrialSiteName, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.ClinicalTrialSubjectID, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.ClinicalTrialSubjectReadingID, RequirementCode.ThreeThree), - }), - }; - } - - /// - /// Reference: https://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.3.2 - /// - /// HashSet of requirements. - private static HashSet GetVisitIdentificationModuleRequirements() - { - return new HashSet - { - new RequirementDetail(DicomTag.InstitutionName, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.InstitutionAddress, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.InstitutionCodeSequence, RequirementCode.ThreeThree, GetCodeSequenceMacroAttributesRequirements()), - new RequirementDetail(DicomTag.InstitutionalDepartmentName, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.InstitutionalDepartmentTypeCodeSequence, RequirementCode.ThreeThree, GetCodeSequenceMacroAttributesRequirements()), - new RequirementDetail(DicomTag.AdmissionID, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.IssuerOfAdmissionIDSequence, RequirementCode.ThreeThree, GetHL7v2HierarchicDesignatorMacroAttributesRequirements()), - new RequirementDetail(DicomTag.ReasonForVisit, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.ReasonForVisitCodeSequence, RequirementCode.ThreeThree, GetCodeSequenceMacroAttributesRequirements()), - new RequirementDetail(DicomTag.ServiceEpisodeID, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.IssuerOfServiceEpisodeIDSequence, RequirementCode.ThreeThree, GetHL7v2HierarchicDesignatorMacroAttributesRequirements()), - new RequirementDetail(DicomTag.ServiceEpisodeDescription, RequirementCode.ThreeThree), - }; - } - - /// - /// Reference: https://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.3.3 - /// - /// HashSet of requirements. - private static HashSet GetVisitStatusModuleRequirements() - { - return new HashSet - { - new RequirementDetail(DicomTag.VisitStatusID, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.CurrentPatientLocation, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.PatientInstitutionResidence, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.VisitComments, RequirementCode.ThreeThree), - }; - } - - /// - /// Reference: https://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.3.4 - /// - /// HashSet of requirements. - private static HashSet GetVisitAdmissionModuleRequirements() - { - return new HashSet() - { - new RequirementDetail(DicomTag.ReferringPhysicianName, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.ReferringPhysicianAddress, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.ReferringPhysicianTelephoneNumbers, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.ReferringPhysicianIdentificationSequence, RequirementCode.ThreeThree, GetPersonIdentificationMacroAttributesRequirements()), - new RequirementDetail(DicomTag.ConsultingPhysicianName, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.ConsultingPhysicianIdentificationSequence, RequirementCode.ThreeThree, GetPersonIdentificationMacroAttributesRequirements()), - new RequirementDetail(DicomTag.AdmittingDiagnosesDescription, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.AdmittingDiagnosesCodeSequence, RequirementCode.ThreeThree, GetCodeSequenceMacroAttributesRequirements()), - new RequirementDetail(DicomTag.RouteOfAdmissions, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.AdmittingDate, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.AdmittingTime, RequirementCode.ThreeThree), - }; - } - - private static HashSet GetUnifiedProcedureStepProgressInformationModuleRequirements(WorkitemRequestType requestType) - { - return new HashSet - { - new RequirementDetail(DicomTag.ProcedureStepState, requestType == WorkitemRequestType.Add ? RequirementCode.OneOne : RequirementCode.NotAllowed), - new RequirementDetail(DicomTag.ProcedureStepProgressInformationSequence, requestType == WorkitemRequestType.Add ? RequirementCode.TwoTwo : RequirementCode.ThreeTwo, new HashSet - { - new RequirementDetail(DicomTag.ProcedureStepProgress, requestType == WorkitemRequestType.Add ? RequirementCode.NotAllowed : RequirementCode.ThreeOne), - new RequirementDetail(DicomTag.ProcedureStepProgressDescription, requestType == WorkitemRequestType.Add ? RequirementCode.NotAllowed : RequirementCode.ThreeOne), - new RequirementDetail(DicomTag.ProcedureStepProgressParametersSequence, requestType == WorkitemRequestType.Add ? RequirementCode.NotAllowed : RequirementCode.ThreeThree, GetProcedureStepProgressParameterSequenceRequirements(requestType)), - new RequirementDetail(DicomTag.ProcedureStepCommunicationsURISequence, requestType == WorkitemRequestType.Add ? RequirementCode.NotAllowed : RequirementCode.ThreeOne, new HashSet - { - new RequirementDetail(DicomTag.ContactURI, requestType == WorkitemRequestType.Add ? RequirementCode.NotAllowed : RequirementCode.OneOne), - new RequirementDetail(DicomTag.ContactDisplayName, requestType == WorkitemRequestType.Add ? RequirementCode.NotAllowed : RequirementCode.ThreeOne), - }), - new RequirementDetail(DicomTag.ProcedureStepCancellationDateTime, requestType == WorkitemRequestType.Add ? RequirementCode.NotAllowed : RequirementCode.ThreeOne), - new RequirementDetail(DicomTag.ReasonForCancellation, requestType == WorkitemRequestType.Add ? RequirementCode.NotAllowed : RequirementCode.ThreeOne), - new RequirementDetail(DicomTag.ProcedureStepDiscontinuationReasonCodeSequence, requestType == WorkitemRequestType.Add ? RequirementCode.NotAllowed : RequirementCode.ThreeOne, GetUPSCodeSequenceMacroRequirements()), - }), - }; - } - - private static HashSet GetUnifiedProcedureStepPerformedProcedureInformationModuleRequirements(WorkitemRequestType requestType) - { - HashSet requirements = new HashSet - { - new RequirementDetail(DicomTag.UnifiedProcedureStepPerformedProcedureSequence, requestType == WorkitemRequestType.Add ? RequirementCode.TwoTwo : RequirementCode.ThreeTwo, new HashSet - { - new RequirementDetail(DicomTag.ActualHumanPerformersSequence, requestType == WorkitemRequestType.Add ? RequirementCode.NotAllowed : RequirementCode.ThreeOne, new HashSet - { - new RequirementDetail(DicomTag.HumanPerformerCodeSequence, requestType == WorkitemRequestType.Add ? RequirementCode.NotAllowed : RequirementCode.ThreeOne, GetUPSCodeSequenceMacroRequirements()), - new RequirementDetail(DicomTag.HumanPerformerName, requestType == WorkitemRequestType.Add ? RequirementCode.NotAllowed : RequirementCode.ThreeOne), - new RequirementDetail(DicomTag.HumanPerformerOrganization, requestType == WorkitemRequestType.Add ? RequirementCode.NotAllowed : RequirementCode.ThreeOne), - }), - new RequirementDetail(DicomTag.PerformedStationNameCodeSequence, requestType == WorkitemRequestType.Add ? RequirementCode.NotAllowed : RequirementCode.ThreeTwo, GetUPSCodeSequenceMacroRequirements()), - new RequirementDetail(DicomTag.PerformedStationClassCodeSequence, requestType == WorkitemRequestType.Add ? RequirementCode.NotAllowed : RequirementCode.ThreeTwo, GetUPSCodeSequenceMacroRequirements()), - new RequirementDetail(DicomTag.PerformedStationGeographicLocationCodeSequence, requestType == WorkitemRequestType.Add ? RequirementCode.NotAllowed : RequirementCode.ThreeTwo, GetUPSCodeSequenceMacroRequirements()), - new RequirementDetail(DicomTag.PerformedProcedureStepStartDateTime, requestType == WorkitemRequestType.Add ? RequirementCode.NotAllowed : RequirementCode.ThreeOne), - new RequirementDetail(DicomTag.PerformedProcedureStepDescription, requestType == WorkitemRequestType.Add ? RequirementCode.NotAllowed : RequirementCode.ThreeOne), - new RequirementDetail(DicomTag.CommentsOnThePerformedProcedureStep, requestType == WorkitemRequestType.Add ? RequirementCode.NotAllowed : RequirementCode.ThreeOne), - new RequirementDetail(DicomTag.PerformedWorkitemCodeSequence, requestType == WorkitemRequestType.Add ? RequirementCode.NotAllowed : RequirementCode.ThreeOne, GetUPSCodeSequenceMacroRequirements()), - new RequirementDetail(DicomTag.PerformedProcessingParametersSequence, requestType == WorkitemRequestType.Add ? RequirementCode.NotAllowed : RequirementCode.ThreeOne, GetUPSContentItemMacroRequirements()), - new RequirementDetail(DicomTag.PerformedProcedureStepEndDateTime, requestType == WorkitemRequestType.Add ? RequirementCode.NotAllowed : RequirementCode.ThreeOne), - new RequirementDetail(DicomTag.OutputInformationSequence, requestType == WorkitemRequestType.Add ? RequirementCode.NotAllowed : RequirementCode.TwoTwo, GetReferencedInstancesAndAccessMacroRequirements()) - }), - }; - - return requirements; - } - - private static HashSet GetCodeSequenceMacroAttributesRequirements() - { - HashSet requirements = new HashSet(GetBasicCodeSequenceMacroAttributesRequirements()); - requirements.Add(new RequirementDetail(DicomTag.EquivalentCodeSequence, RequirementCode.ThreeThree, GetEquivalentCodeSequenceRequirements())); - requirements.UnionWith(GetEnhancedCodeSequenceMacroAttributesRequirements()); - return requirements; - } - - private static HashSet GetBasicCodeSequenceMacroAttributesRequirements() - { - return new HashSet - { - new RequirementDetail(DicomTag.CodeValue, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.CodingSchemeDesignator, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.CodingSchemeVersion, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.CodeMeaning, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.LongCodeValue, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.URNCodeValue, RequirementCode.ThreeThree), - }; - } - - private static HashSet GetEnhancedCodeSequenceMacroAttributesRequirements() - { - return new HashSet - { - new RequirementDetail(DicomTag.ContextIdentifier, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.ContextUID, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.MappingResource, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.MappingResourceUID, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.MappingResourceName, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.ContextGroupVersion, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.ContextGroupExtensionFlag, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.ContextGroupLocalVersion, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.ContextGroupExtensionCreatorUID, RequirementCode.ThreeThree), - }; - } - - private static HashSet GetEquivalentCodeSequenceRequirements() - { - HashSet requirements = new HashSet(GetBasicCodeSequenceMacroAttributesRequirements()); - requirements.UnionWith(GetEnhancedCodeSequenceMacroAttributesRequirements()); - return requirements; - } - - private static HashSet GetPersonIdentificationMacroAttributesRequirements() - { - return new HashSet - { - new RequirementDetail(DicomTag.PersonIdentificationCodeSequence, RequirementCode.ThreeThree, GetCodeSequenceMacroAttributesRequirements()), - new RequirementDetail(DicomTag.PersonAddress, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.PersonTelephoneNumbers, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.PersonTelecomInformation, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.InstitutionName, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.InstitutionAddress, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.InstitutionCodeSequence, RequirementCode.ThreeThree, GetCodeSequenceMacroAttributesRequirements()), - new RequirementDetail(DicomTag.InstitutionalDepartmentName, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.InstitutionalDepartmentTypeCodeSequence, RequirementCode.ThreeThree, GetCodeSequenceMacroAttributesRequirements()), - }; - } - - private static HashSet GetDigitalSignatureMacroAttributesRequirements() - { - return new HashSet - { - new RequirementDetail(DicomTag.MACParametersSequence, RequirementCode.ThreeThree, new HashSet - { - new RequirementDetail(DicomTag.MACIDNumber, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.MACCalculationTransferSyntaxUID, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.MACAlgorithm, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.DataElementsSigned, RequirementCode.ThreeThree), - }), - new RequirementDetail(DicomTag.DigitalSignaturesSequence, RequirementCode.ThreeThree, new HashSet - { - new RequirementDetail(DicomTag.MACIDNumber, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.DigitalSignatureUID, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.DigitalSignatureDateTime, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.CertificateType, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.CertificateOfSigner, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.Signature, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.CertifiedTimestampType, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.CertifiedTimestamp, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.DigitalSignaturePurposeCodeSequence, RequirementCode.ThreeThree, GetCodeSequenceMacroAttributesRequirements()), - }), - }; - } - - private static RequirementDetail GetOriginalAttributesMacroAttributesRequirements() - { - return new RequirementDetail(DicomTag.OriginalAttributesSequence, RequirementCode.ThreeThree, new HashSet - { - new RequirementDetail(DicomTag.SourceOfPreviousValues, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.AttributeModificationDateTime, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.ModifyingSystem, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.ReasonForTheAttributeModification, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.ModifiedAttributesSequence, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.NonconformingModifiedAttributesSequence, RequirementCode.ThreeThree, new HashSet - { - new RequirementDetail(DicomTag.SelectorAttribute, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.SelectorValueNumber, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.SelectorSequencePointer, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.SelectorSequencePointerPrivateCreator, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.SelectorSequencePointerItems, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.SelectorAttributePrivateCreator, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.NonconformingDataElementValue, RequirementCode.ThreeThree), - }), - }); - } - - private static RequirementDetail GetHL7StructuredDocumentReferenceSequenceRequirements() - { - return new RequirementDetail(DicomTag.HL7StructuredDocumentReferenceSequence, RequirementCode.ThreeThree, new HashSet - { - new RequirementDetail(DicomTag.ReferencedSOPClassUID, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.ReferencedSOPInstanceUID, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.HL7InstanceIdentifier, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.RetrieveURI, RequirementCode.ThreeThree), - }); - } - - private static RequirementDetail GetConversionSourceAttributesSequenceRequirements() - { - return new RequirementDetail(DicomTag.ConversionSourceAttributesSequence, RequirementCode.ThreeThree, new HashSet - { - new RequirementDetail(DicomTag.ReferencedSOPClassUID, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.ReferencedSOPInstanceUID, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.ReferencedFrameNumber, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.ReferencedSegmentNumber, RequirementCode.ThreeThree), - }); - } - - private static HashSet GetGeneralProcedureProtocolReferenceMacroAttributesRequirements() - { - return new HashSet - { - new RequirementDetail(DicomTag.ReferencedDefinedProtocolSequence, RequirementCode.ThreeThree, GetReferencedProtocolSequenceRequirements()), - new RequirementDetail(DicomTag.ReferencedPerformedProtocolSequence, RequirementCode.ThreeThree, GetReferencedProtocolSequenceRequirements()), - }; - } - - private static HashSet GetReferencedProtocolSequenceRequirements() - { - return new HashSet - { - new RequirementDetail(DicomTag.ReferencedSOPClassUID, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.ReferencedSOPInstanceUID, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.SourceAcquisitionProtocolElementNumber, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.SourceReconstructionProtocolElementNumber, RequirementCode.ThreeThree), - }; - } - - private static HashSet GetUPSContentItemMacroRequirements() - { - return new HashSet - { - new RequirementDetail(DicomTag.ValueType, RequirementCode.OneOne), - new RequirementDetail(DicomTag.ConceptNameCodeSequence, RequirementCode.OneOne, GetUPSCodeSequenceMacroRequirements()), - new RequirementDetail(DicomTag.DateTime, RequirementCode.OneCOneC), - new RequirementDetail(DicomTag.Date, RequirementCode.OneCOneC), - new RequirementDetail(DicomTag.Time, RequirementCode.OneCOneC), - new RequirementDetail(DicomTag.PersonName, RequirementCode.OneCOneC), - new RequirementDetail(DicomTag.UID, RequirementCode.OneCOneC), - new RequirementDetail(DicomTag.TextValue, RequirementCode.OneCOneC), - new RequirementDetail(DicomTag.ConceptCodeSequence, RequirementCode.OneCOneC, GetUPSCodeSequenceMacroRequirements()), - new RequirementDetail(DicomTag.NumericValue, RequirementCode.OneCOneC), - new RequirementDetail(DicomTag.MeasurementUnitsCodeSequence, RequirementCode.OneCOneC, GetUPSCodeSequenceMacroRequirements()), - }; - } - - /// - /// https://dicom.nema.org/medical/dicom/current/output/html/part04.html#table_CC.2.5-2a - /// - /// - private static HashSet GetUPSCodeSequenceMacroRequirements() - { - return new HashSet - { - new RequirementDetail(DicomTag.CodeValue, RequirementCode.OneCOneC), - new RequirementDetail(DicomTag.CodingSchemeDesignator, RequirementCode.OneCOneC), - new RequirementDetail(DicomTag.CodingSchemeVersion, RequirementCode.OneCOneC), - new RequirementDetail(DicomTag.CodeMeaning, RequirementCode.OneOne), - new RequirementDetail(DicomTag.LongCodeValue, RequirementCode.OneCOneC), - new RequirementDetail(DicomTag.URNCodeValue, RequirementCode.OneCOneC), - new RequirementDetail(DicomTag.MappingResource, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.MappingResourceUID, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.ContextGroupVersion, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.ContextGroupExtensionFlag, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.ContextGroupLocalVersion, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.ContextGroupExtensionCreatorUID, RequirementCode.ThreeThree), - }; - } - - private static HashSet GetReferencedInstancesAndAccessMacroRequirements() - { - return new HashSet - { - new RequirementDetail(DicomTag.TypeOfInstances, RequirementCode.OneOne), - new RequirementDetail(DicomTag.StudyInstanceUID, RequirementCode.OneCOne), - new RequirementDetail(DicomTag.SeriesInstanceUID, RequirementCode.OneCOne), - new RequirementDetail(DicomTag.ReferencedSOPSequence, RequirementCode.OneOne, new HashSet - { - new RequirementDetail(DicomTag.ReferencedSOPClassUID, RequirementCode.OneOne), - new RequirementDetail(DicomTag.ReferencedSOPInstanceUID, RequirementCode.OneOne), - new RequirementDetail(DicomTag.HL7InstanceIdentifier, RequirementCode.OneCOne), - new RequirementDetail(DicomTag.ReferencedFrameNumber, RequirementCode.OneCOne), - new RequirementDetail(DicomTag.ReferencedSegmentNumber, RequirementCode.OneCOne), - }), - new RequirementDetail(DicomTag.DICOMRetrievalSequence, RequirementCode.OneCOne, new HashSet - { - new RequirementDetail(DicomTag.RetrieveAETitle, RequirementCode.OneOne), - }), - new RequirementDetail(DicomTag.DICOMMediaRetrievalSequence, RequirementCode.OneCOne, new HashSet - { - new RequirementDetail(DicomTag.StorageMediaFileSetID, RequirementCode.TwoTwo), - new RequirementDetail(DicomTag.StorageMediaFileSetUID, RequirementCode.OneOne), - }), - new RequirementDetail(DicomTag.WADORetrievalSequence, RequirementCode.OneCOne, new HashSet - { - new RequirementDetail(DicomTag.RetrieveURI, RequirementCode.OneOne), - }), - new RequirementDetail(DicomTag.XDSRetrievalSequence, RequirementCode.OneCOne, new HashSet - { - new RequirementDetail(DicomTag.RepositoryUniqueID, RequirementCode.OneOne), - new RequirementDetail(DicomTag.HomeCommunityID, RequirementCode.ThreeTwo), - }), - new RequirementDetail(DicomTag.WADORSRetrievalSequence, RequirementCode.OneCOne, new HashSet - { - new RequirementDetail(DicomTag.RetrieveURL, RequirementCode.OneOne), - }), - }; - } - - private static HashSet GetOtherPatientIDSequenceRequirements(WorkitemRequestType requestType) - { - HashSet requirements = new HashSet - { - new RequirementDetail(DicomTag.PatientID, RequirementCode.OneOne), - }; - - requirements.UnionWith(GetIssuerOfPatientIDMacroRequirements(requestType)); - - requirements.Add(new RequirementDetail(DicomTag.TypeOfPatientID, RequirementCode.ThreeThree)); - - return requirements; - } - - /// - /// Reference: https://dicom.nema.org/medical/dicom/current/output/html/part04.html#table_CC.2.5-2e - /// - /// - /// HashSet of requirement detail. - private static HashSet GetIssuerOfPatientIDMacroRequirements(WorkitemRequestType requestType) - { - return new HashSet - { - new RequirementDetail(DicomTag.IssuerOfPatientID, requestType == WorkitemRequestType.Add ? RequirementCode.TwoTwo : RequirementCode.NotAllowed), - new RequirementDetail(DicomTag.IssuerOfPatientIDQualifiersSequence, requestType == WorkitemRequestType.Add ? RequirementCode.TwoTwo : RequirementCode.NotAllowed, new HashSet - { - new RequirementDetail(DicomTag.UniversalEntityID, RequirementCode.TwoTwo), - new RequirementDetail(DicomTag.UniversalEntityIDType, RequirementCode.OneCOne), - new RequirementDetail(DicomTag.IdentifierTypeCode, RequirementCode.TwoTwo), - new RequirementDetail(DicomTag.AssigningFacilitySequence, RequirementCode.TwoTwo, GetHL7v2HierarchicDesignatorMacroForAddRequirements()), - new RequirementDetail(DicomTag.AssigningJurisdictionCodeSequence, RequirementCode.TwoTwo, GetUPSCodeSequenceMacroRequirements()), - new RequirementDetail(DicomTag.AssigningAgencyOrDepartmentCodeSequence, RequirementCode.TwoTwo, GetUPSCodeSequenceMacroRequirements()), - }), - }; - } - - private static HashSet GetPrimaryLanguageCodeSequenceRequirements() - { - HashSet requirements = new HashSet(GetCodeSequenceMacroAttributesRequirements()); - requirements.Add(new RequirementDetail(DicomTag.PatientPrimaryLanguageModifierCodeSequence, RequirementCode.ThreeThree, GetCodeSequenceMacroAttributesRequirements())); - return requirements; - } - - /// - /// Reference: https://dicom.nema.org/medical/dicom/current/output/html/part03.html#table_10-17 - /// - /// HashSet of requirement details. - private static HashSet GetHL7v2HierarchicDesignatorMacroAttributesRequirements() - { - return new HashSet - { - new RequirementDetail(DicomTag.LocalNamespaceEntityID, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.UniversalEntityID, RequirementCode.ThreeThree), - new RequirementDetail(DicomTag.UniversalEntityIDType, RequirementCode.ThreeThree), - }; - } - - private static HashSet GetProcedureStepProgressParameterSequenceRequirements(WorkitemRequestType requestType) - { - HashSet requirements = new HashSet(GetUPSContentItemMacroRequirements()); - requirements.Add(new RequirementDetail(DicomTag.ContentItemModifierSequence, requestType == WorkitemRequestType.Add ? RequirementCode.NotAllowed : RequirementCode.ThreeThree, GetUPSContentItemMacroRequirements())); - return requirements; - } - - /// - /// Reference: https://dicom.nema.org/medical/dicom/current/output/html/part04.html#table_CC.2.5-2d - /// - /// Hashset of requirement details. - private static HashSet GetHL7v2HierarchicDesignatorMacroForAddRequirements() - { - return new HashSet - { - new RequirementDetail(DicomTag.LocalNamespaceEntityID, RequirementCode.OneCOne), - new RequirementDetail(DicomTag.UniversalEntityID, RequirementCode.OneCOne), - new RequirementDetail(DicomTag.UniversalEntityIDType, RequirementCode.OneCOne), - }; - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemFinalStateValidatorExtension.cs b/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemFinalStateValidatorExtension.cs deleted file mode 100644 index 9aa944207d..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemFinalStateValidatorExtension.cs +++ /dev/null @@ -1,118 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Workitem.Model; - -namespace Microsoft.Health.Dicom.Core.Features.Workitem; - -/// -/// Workitem final-state validator extension -/// -internal static class WorkitemFinalStateValidatorExtension -{ - private static readonly HashSet Requirements = GetRequirements(); - - public static void ValidateFinalStateRequirement(this DicomDataset dataset) - { - var procedureStepState = dataset.GetProcedureStepState(); - - foreach (var requirement in Requirements) - { - dataset.ValidateRequirement(requirement.DicomTag, procedureStepState, requirement.RequirementCode); - - if (null != requirement.SequenceRequirements) - { - dataset.ValidateSequence(requirement.DicomTag, procedureStepState, requirement.SequenceRequirements); - } - } - } - - private static void ValidateSequence(this DicomDataset dataset, DicomTag sequenceTag, ProcedureStepState procedureStepState, IReadOnlyCollection requirements) - { - if (requirements.Count == 0 || !dataset.TryGetSequence(sequenceTag, out var sequence) || sequence.Items.Count == 0) - { - return; - } - - foreach (var sequenceDataset in sequence.Items) - { - foreach (var requirement in requirements) - { - sequenceDataset.ValidateRequirement(requirement.DicomTag, procedureStepState, requirement.RequirementCode); - - if (null != requirement.SequenceRequirements) - { - sequenceDataset.ValidateSequence(requirement.DicomTag, procedureStepState, requirement.SequenceRequirements); - } - } - } - } - - /// - /// Refer - /// - /// - private static HashSet GetRequirements() - { - var map = new HashSet - { - new FinalStateRequirementDetail(DicomTag.TransactionUID, FinalStateRequirementCode.O), - - // SOP Common Module - - // Refer: https://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.12.html#sect_C.12.1.1.2 - // Intentionally changed to Optional, until we support character sets. - new FinalStateRequirementDetail(DicomTag.SpecificCharacterSet, FinalStateRequirementCode.O), - new FinalStateRequirementDetail(DicomTag.SOPClassUID, FinalStateRequirementCode.R), - new FinalStateRequirementDetail(DicomTag.SOPInstanceUID, FinalStateRequirementCode.R), - - // Unified Procedure Step Scheduled Procedure Information Module - new FinalStateRequirementDetail(DicomTag.ScheduledProcedureStepPriority, FinalStateRequirementCode.R), - new FinalStateRequirementDetail(DicomTag.ScheduledProcedureStepModificationDateTime, FinalStateRequirementCode.R), - new FinalStateRequirementDetail(DicomTag.ScheduledProcedureStepStartDateTime, FinalStateRequirementCode.R), - new FinalStateRequirementDetail(DicomTag.InputReadinessState, FinalStateRequirementCode.R), - - // Unified Procedure Step Relationship Module - - // Patient Demographic Module - - // Patient Medical Module - - // Visit Identification Module - - // Visit Status Module - - // Visit Admission Module - - // Unified Procedure Step Progress Information Module - new FinalStateRequirementDetail(DicomTag.ProcedureStepState, FinalStateRequirementCode.R), - new FinalStateRequirementDetail(DicomTag.ProcedureStepProgressInformationSequence, FinalStateRequirementCode.X, new HashSet - { - new FinalStateRequirementDetail(DicomTag.ProcedureStepCancellationDateTime, FinalStateRequirementCode.X), - new FinalStateRequirementDetail(DicomTag.ProcedureStepDiscontinuationReasonCodeSequence, FinalStateRequirementCode.X), - }), - - // Unified Procedure Step Performed Procedure Information Module - new FinalStateRequirementDetail(DicomTag.UnifiedProcedureStepPerformedProcedureSequence, FinalStateRequirementCode.P, new HashSet - { - new FinalStateRequirementDetail(DicomTag.ActualHumanPerformersSequence, FinalStateRequirementCode.RC, new HashSet - { - new FinalStateRequirementDetail(DicomTag.HumanPerformerCodeSequence, FinalStateRequirementCode.RC), - new FinalStateRequirementDetail(DicomTag.HumanPerformerName, FinalStateRequirementCode.RC), - }), - new FinalStateRequirementDetail(DicomTag.PerformedStationNameCodeSequence, FinalStateRequirementCode.P), - new FinalStateRequirementDetail(DicomTag.PerformedProcedureStepStartDateTime, FinalStateRequirementCode.P), - new FinalStateRequirementDetail(DicomTag.PerformedWorkitemCodeSequence, FinalStateRequirementCode.P), - new FinalStateRequirementDetail(DicomTag.PerformedProcedureStepEndDateTime, FinalStateRequirementCode.P), - new FinalStateRequirementDetail(DicomTag.OutputInformationSequence, FinalStateRequirementCode.P), - }), - }; - - return map; - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemOrchestrator.cs b/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemOrchestrator.cs deleted file mode 100644 index 3bfc5e76a8..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemOrchestrator.cs +++ /dev/null @@ -1,344 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Data; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using FellowOakDicom; -using Microsoft.Extensions.Logging; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Context; -using Microsoft.Health.Dicom.Core.Features.Query; -using Microsoft.Health.Dicom.Core.Features.Query.Model; -using Microsoft.Health.Dicom.Core.Features.Store; -using Microsoft.Health.Dicom.Core.Features.Workitem.Model; -using Microsoft.Health.Dicom.Core.Messages.Workitem; - -namespace Microsoft.Health.Dicom.Core.Features.Workitem; - -/// -/// Provides functionality to orchestrate the DICOM workitem instance add, retrieve, cancel, and update. -/// -public class WorkitemOrchestrator : IWorkitemOrchestrator -{ - private readonly IDicomRequestContextAccessor _contextAccessor; - private readonly IIndexWorkitemStore _indexWorkitemStore; - private readonly IWorkitemStore _workitemStore; - private readonly IWorkitemQueryTagService _workitemQueryTagService; - private readonly ILogger _logger; - private readonly IQueryParser _queryParser; - - public WorkitemOrchestrator( - IDicomRequestContextAccessor contextAccessor, - IWorkitemStore workitemStore, - IIndexWorkitemStore indexWorkitemStore, - IWorkitemQueryTagService workitemQueryTagService, - IQueryParser queryParser, - ILogger logger) - { - _contextAccessor = EnsureArg.IsNotNull(contextAccessor, nameof(contextAccessor)); - _indexWorkitemStore = EnsureArg.IsNotNull(indexWorkitemStore, nameof(indexWorkitemStore)); - _workitemStore = EnsureArg.IsNotNull(workitemStore, nameof(workitemStore)); - _queryParser = EnsureArg.IsNotNull(queryParser, nameof(queryParser)); - _workitemQueryTagService = EnsureArg.IsNotNull(workitemQueryTagService, nameof(workitemQueryTagService)); - _logger = EnsureArg.IsNotNull(logger, nameof(logger)); - } - - /// - public async Task GetWorkitemMetadataAsync(string workitemUid, CancellationToken cancellationToken = default) - { - var partitionKey = _contextAccessor.RequestContext.GetPartitionKey(); - - var workitemMetadata = await _indexWorkitemStore - .GetWorkitemMetadataAsync(partitionKey, workitemUid, cancellationToken) - .ConfigureAwait(false); - - return workitemMetadata; - } - - /// - public async Task AddWorkitemAsync(DicomDataset dataset, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(dataset, nameof(dataset)); - - WorkitemInstanceIdentifier identifier = null; - - try - { - var partitionKey = _contextAccessor.RequestContext.GetPartitionKey(); - var queryTags = await _workitemQueryTagService.GetQueryTagsAsync(cancellationToken).ConfigureAwait(false); - - identifier = await _indexWorkitemStore - .BeginAddWorkitemAsync(partitionKey, dataset, queryTags, cancellationToken) - .ConfigureAwait(false); - - // We have successfully created the index, store the file. - await StoreWorkitemBlobAsync(identifier, dataset, null, cancellationToken) - .ConfigureAwait(false); - - await _indexWorkitemStore - .EndAddWorkitemAsync(identifier.PartitionKey, identifier.WorkitemKey, cancellationToken) - .ConfigureAwait(false); - } - catch - { - await TryAddWorkitemCleanupAsync(identifier, cancellationToken) - .ConfigureAwait(false); - - throw; - } - } - - /// - public async Task UpdateWorkitemStateAsync(DicomDataset dataset, WorkitemMetadataStoreEntry workitemMetadata, ProcedureStepState targetProcedureStepState, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(dataset, nameof(dataset)); - EnsureArg.IsNotNull(workitemMetadata, nameof(workitemMetadata)); - - // Check if the workitem is locked for read-write - if (workitemMetadata.Status != WorkitemStoreStatus.ReadWrite) - { - throw new DataStoreException(DicomCoreResource.WorkitemCurrentlyBeingUpdated); - } - - (long CurrentWatermark, long NextWatermark)? watermarkEntry = null; - - try - { - // Get the current and next watermarks for the workitem instance - watermarkEntry = await _indexWorkitemStore - .GetCurrentAndNextWorkitemWatermarkAsync(workitemMetadata.WorkitemKey, cancellationToken) - .ConfigureAwait(false); - - if (!watermarkEntry.HasValue) - { - throw new DataStoreException(DicomCoreResource.DataStoreOperationFailed); - } - - // store the blob with the new watermark - await StoreWorkitemBlobAsync(workitemMetadata, dataset, watermarkEntry.Value.NextWatermark, cancellationToken) - .ConfigureAwait(false); - - dataset.TryGetString(DicomTag.TransactionUID, out var transactionUid); - - // Update the workitem procedure step state in the store - await _indexWorkitemStore - .UpdateWorkitemProcedureStepStateAsync( - workitemMetadata, - watermarkEntry.Value.NextWatermark, - targetProcedureStepState.GetStringValue(), - transactionUid ?? workitemMetadata.TransactionUid, - cancellationToken) - .ConfigureAwait(false); - - // Delete the blob with the old watermark - await TryDeleteWorkitemBlobAsync(workitemMetadata, watermarkEntry.Value.CurrentWatermark, cancellationToken) - .ConfigureAwait(false); - } - catch - { - // attempt to delete the blob with proposed watermark - if (watermarkEntry.HasValue) - { - await TryDeleteWorkitemBlobAsync(workitemMetadata, watermarkEntry.Value.NextWatermark, cancellationToken) - .ConfigureAwait(false); - } - - throw; - } - } - - /// - public async Task UpdateWorkitemAsync(DicomDataset dataset, WorkitemMetadataStoreEntry workitemMetadata, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(dataset, nameof(dataset)); - EnsureArg.IsNotNull(workitemMetadata, nameof(workitemMetadata)); - - if (workitemMetadata.Status != WorkitemStoreStatus.ReadWrite) - { - throw new DataStoreException(DicomCoreResource.WorkitemCurrentlyBeingUpdated); - } - - (long CurrentWatermark, long NextWatermark)? watermarkEntry = null; - - // Get the current and next watermarks for the workitem instance - watermarkEntry = await _indexWorkitemStore - .GetCurrentAndNextWorkitemWatermarkAsync(workitemMetadata.WorkitemKey, cancellationToken) - .ConfigureAwait(false); - - if (!watermarkEntry.HasValue) - { - throw new DataStoreException(DicomCoreResource.DataStoreOperationFailed); - } - - // store the blob with the new watermark. - await StoreWorkitemBlobAsync(workitemMetadata, dataset, watermarkEntry.Value.NextWatermark, cancellationToken) - .ConfigureAwait(false); - - try - { - var queryTags = await _workitemQueryTagService.GetQueryTagsAsync(cancellationToken).ConfigureAwait(false); - - // Update details in Sql Server. - // Update the workitem watermark in the store. - // Update extended query tag tables. - await _indexWorkitemStore - .UpdateWorkitemTransactionAsync( - workitemMetadata, - watermarkEntry.Value.NextWatermark, - dataset, - queryTags, - cancellationToken) - .ConfigureAwait(false); - } - catch - { - // attempt to delete the blob with proposed watermark - if (watermarkEntry.HasValue) - { - await TryDeleteWorkitemBlobAsync(workitemMetadata, watermarkEntry.Value.NextWatermark, cancellationToken) - .ConfigureAwait(false); - } - - throw new DataStoreException(DicomCoreResource.UpdateWorkitemInstanceConflictFailure, FailureReasonCodes.UpsUpdateConflict); - } - - // Delete the blob with the old watermark - await TryDeleteWorkitemBlobAsync(workitemMetadata, watermarkEntry.Value.CurrentWatermark, cancellationToken) - .ConfigureAwait(false); - } - - /// - public async Task QueryAsync(BaseQueryParameters parameters, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(parameters); - - var queryTags = await _workitemQueryTagService - .GetQueryTagsAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - - BaseQueryExpression queryExpression = _queryParser.Parse(parameters, queryTags); - - var partitionKey = _contextAccessor.RequestContext.GetPartitionKey(); - - WorkitemQueryResult queryResult = await _indexWorkitemStore - .QueryAsync(partitionKey, queryExpression, cancellationToken) - .ConfigureAwait(false); - - var workitemTasks = queryResult.WorkitemInstances - .Select(x => TryGetWorkitemBlobAsync(x, cancellationToken)); - - IEnumerable workitems = await Task - .WhenAll(workitemTasks) - .ConfigureAwait(false); - - return WorkitemQueryResponseBuilder.BuildWorkitemQueryResponse(workitems.ToList(), queryExpression); - } - - /// - public async Task GetWorkitemBlobAsync(WorkitemInstanceIdentifier identifier, CancellationToken cancellationToken = default) - { - if (identifier == null) - { - return null; - } - - return await _workitemStore - .GetWorkitemAsync(identifier, cancellationToken) - .ConfigureAwait(false); - } - - /// - public async Task RetrieveWorkitemAsync(WorkitemInstanceIdentifier workitemInstanceIdentifier, CancellationToken cancellationToken = default) - { - EnsureArg.IsNotNull(workitemInstanceIdentifier, nameof(workitemInstanceIdentifier)); - - return await _workitemStore - .GetWorkitemAsync(workitemInstanceIdentifier, cancellationToken) - .ConfigureAwait(false); - } - - private async Task TryGetWorkitemBlobAsync(WorkitemInstanceIdentifier identifier, CancellationToken cancellationToken) - { - try - { - return await GetWorkitemBlobAsync(identifier, cancellationToken) - .ConfigureAwait(false); - } - catch (ItemNotFoundException ex) - { - _logger.LogWarning(ex, "Workitem [{Identifier}] blob doesn't exist due to simultaneous GET and UPDATE request or it could just be missing.", identifier); - return null; - } - } - - /// - [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Will reevaluate exceptions when standardizing deletion exceptions.")] - private async Task TryAddWorkitemCleanupAsync(WorkitemInstanceIdentifier identifier, CancellationToken cancellationToken) - { - if (identifier == null) - { - return; - } - - try - { - // Cleanup workitem data store - await _indexWorkitemStore - .DeleteWorkitemAsync(identifier, cancellationToken) - .ConfigureAwait(false); - - // Cleanup Blob store - await TryDeleteWorkitemBlobAsync(identifier, null, cancellationToken) - .ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogWarning(ex, @"Failed to cleanup workitem [{Identifier}].", identifier); - } - } - - private async Task StoreWorkitemBlobAsync( - WorkitemInstanceIdentifier identifier, - DicomDataset dicomDataset, - long? proposedWatermark = default, - CancellationToken cancellationToken = default) - { - if (identifier == null || dicomDataset == null) - { - return; - } - - await _workitemStore - .AddWorkitemAsync(identifier, dicomDataset, proposedWatermark, cancellationToken) - .ConfigureAwait(false); - } - - [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Will reevaluate exceptions when standardizing deletion exceptions.")] - private async Task TryDeleteWorkitemBlobAsync(WorkitemInstanceIdentifier identifier, long? proposedWatermark = default, CancellationToken cancellationToken = default) - { - if (identifier == null) - { - return; - } - - try - { - await _workitemStore - .DeleteWorkitemAsync(identifier, proposedWatermark, cancellationToken) - .ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogWarning(ex, @"Failed to delete workitem blob for [{Identifier}].", identifier); - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemQueryParser.cs b/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemQueryParser.cs deleted file mode 100644 index 0937e26936..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemQueryParser.cs +++ /dev/null @@ -1,159 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using EnsureThat; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Features.Query.Model; -using Microsoft.Health.Dicom.Core.Features.Workitem; - -namespace Microsoft.Health.Dicom.Core.Features.Query; - -/// -/// Main parser class that converts uri query parameters to sql ready query expresions for workitem search request -/// -public class WorkitemQueryParser : BaseQueryParser -{ - private readonly IDicomTagParser _dicomTagPathParser; - - public WorkitemQueryParser(IDicomTagParser dicomTagPathParser) - => _dicomTagPathParser = EnsureArg.IsNotNull(dicomTagPathParser, nameof(dicomTagPathParser)); - - public override BaseQueryExpression Parse(BaseQueryParameters parameters, IReadOnlyCollection queryTags) - { - EnsureArg.IsNotNull(parameters, nameof(parameters)); - EnsureArg.IsNotNull(queryTags, nameof(queryTags)); - - var filterConditions = new Dictionary(); - foreach (KeyValuePair filter in parameters.Filters) - { - // filter conditions with attributeId as key - if (!ParseFilterCondition(filter, queryTags, parameters.FuzzyMatching, out QueryFilterCondition condition)) - { - throw new QueryParseException(string.Format(CultureInfo.CurrentCulture, DicomCoreResource.UnsupportedSearchParameter, filter.Key)); - } - - if (!filterConditions.TryAdd(condition.QueryTag.WorkitemQueryTagStoreEntry.Key, condition)) - { - throw new QueryParseException(string.Format(CultureInfo.CurrentCulture, DicomCoreResource.DuplicateAttribute, filter.Key)); - } - } - - return new BaseQueryExpression( - ParseIncludeFields(parameters.IncludeField), - parameters.FuzzyMatching, - parameters.Limit, - parameters.Offset, - filterConditions.Values); - } - - private bool ParseFilterCondition( - KeyValuePair queryParameter, - IEnumerable queryTags, - bool fuzzyMatching, - out QueryFilterCondition condition) - { - condition = null; - - // parse tag - if (!TryParseDicomAttributeId(queryParameter.Key, out DicomTag[] dicomTags)) - { - return false; - } - - QueryTag queryTag = GetMatchingQueryTag(dicomTags, queryParameter.Key, queryTags); - - if (string.IsNullOrWhiteSpace(queryParameter.Value)) - { - throw new QueryParseException(string.Format(CultureInfo.CurrentCulture, DicomCoreResource.QueryEmptyAttributeValue, queryParameter.Key)); - } - - if (!TryGetValueParser(queryTag, fuzzyMatching, out Func valueParser)) - { - return false; - } - - condition = valueParser(queryTag, queryParameter.Value); - return true; - } - - private bool TryParseDicomAttributeId(string attributeId, out DicomTag[] dicomTags) - { - if (_dicomTagPathParser.TryParse(attributeId, out DicomTag[] result, supportMultiple: true)) - { - dicomTags = result; - return true; - } - - dicomTags = null; - return false; - } - - private static QueryTag GetMatchingQueryTag(DicomTag[] dicomTags, string attributeId, IEnumerable queryTags) - { - if (dicomTags.Length > 2) - { - throw new QueryParseException(string.Format(CultureInfo.CurrentCulture, DicomCoreResource.NestedSequencesNotSupported, attributeId)); - } - - QueryTag queryTag = queryTags.FirstOrDefault(item => - { - return Enumerable.SequenceEqual(dicomTags, item.WorkitemQueryTagStoreEntry.PathTags); - }); - - if (queryTag == null) - { - throw new QueryParseException(string.Format(CultureInfo.CurrentCulture, DicomCoreResource.UnsupportedSearchParameter, attributeId)); - } - - // Currently only 2 level of sequence tags are supported, so always taking the last element to create a new query tag - var dicomTag = dicomTags.LastOrDefault(); - var entry = new WorkitemQueryTagStoreEntry(queryTag.WorkitemQueryTagStoreEntry.Key, dicomTag.GetPath(), dicomTag.GetDefaultVR().Code) - { - PathTags = Array.AsReadOnly(new DicomTag[] { dicomTag }) - }; - - return new QueryTag(entry); - } - - private QueryIncludeField ParseIncludeFields(IReadOnlyList includeFields) - { - // Check if "all" is present as one of the values in IncludeField parameter. - if (includeFields.Any(val => IncludeFieldValueAll.Equals(val, StringComparison.OrdinalIgnoreCase))) - { - if (includeFields.Count > 1) - { - throw new QueryParseException(DicomCoreResource.InvalidIncludeAllFields); - } - - return QueryIncludeField.AllFields; - } - - var fields = new List(includeFields.Count); - foreach (string field in includeFields) - { - if (!TryParseDicomAttributeId(field, out DicomTag[] dicomTags)) - { - throw new QueryParseException(string.Format(CultureInfo.CurrentCulture, DicomCoreResource.IncludeFieldUnknownAttribute, field)); - } - - if (dicomTags.Length > 1) - { - throw new QueryParseException(DicomCoreResource.SequentialDicomTagsNotSupported); - } - - // For now only first level tags are supported - fields.Add(dicomTags[0]); - } - - return new QueryIncludeField(fields); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemQueryResponseBuilder.cs b/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemQueryResponseBuilder.cs deleted file mode 100644 index e02fa180e5..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemQueryResponseBuilder.cs +++ /dev/null @@ -1,127 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Linq; -using EnsureThat; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Features.Query.Model; -using Microsoft.Health.Dicom.Core.Messages.Workitem; - -namespace Microsoft.Health.Dicom.Core.Features.Workitem; - -public static class WorkitemQueryResponseBuilder -{ - /// - /// Workitem attributes with a return key type of 1 or 2 (including conditionals). - /// . - /// - public static readonly HashSet RequiredReturnTags = new HashSet - { - // SOP Common Module - DicomTag.SpecificCharacterSet, - DicomTag.SOPClassUID, - DicomTag.SOPInstanceUID, - - // Unified Procedure Step Scheduled Procedure Information Module - DicomTag.ScheduledProcedureStepPriority, - DicomTag.ProcedureStepLabel, - DicomTag.WorklistLabel, - DicomTag.ScheduledProcessingParametersSequence, - DicomTag.ScheduledStationNameCodeSequence, - DicomTag.ScheduledStationClassCodeSequence, - DicomTag.ScheduledStationGeographicLocationCodeSequence, - DicomTag.ScheduledHumanPerformersSequence, - DicomTag.ScheduledProcedureStepStartDateTime, - DicomTag.ScheduledWorkitemCodeSequence, - DicomTag.InputReadinessState, - DicomTag.InputInformationSequence, - DicomTag.StudyInstanceUID, - - // Unified Procedure Step Relationship Module - DicomTag.PatientName, - DicomTag.PatientID, - - // Issuer of Patient ID Macro - DicomTag.IssuerOfPatientID, - DicomTag.IssuerOfPatientIDQualifiersSequence, - - DicomTag.OtherPatientIDsSequence, - DicomTag.PatientBirthDate, - DicomTag.PatientSex, - DicomTag.AdmissionID, - DicomTag.IssuerOfAdmissionIDSequence, - DicomTag.AdmittingDiagnosesDescription, - DicomTag.AdmittingDiagnosesCodeSequence, - DicomTag.ReferencedRequestSequence, - - // Patient Medical Module - DicomTag.MedicalAlerts, - DicomTag.PregnancyStatus, - DicomTag.SpecialNeeds, - - // Unified Procedure Step Progress Information Module - DicomTag.ProcedureStepState, - DicomTag.ProcedureStepProgressInformationSequence, - }; - - /// - /// Builds workitem query response - /// - /// - public static QueryWorkitemResourceResponse BuildWorkitemQueryResponse(IReadOnlyList datasets, BaseQueryExpression queryExpression) - { - var status = WorkitemResponseStatus.NoContent; - - if (datasets.Any(x => x == null)) - { - status = WorkitemResponseStatus.PartialContent; - } - else if (datasets.Any()) - { - status = WorkitemResponseStatus.Success; - } - - var workitemResponses = datasets.Where(x => x != null).Select(m => GenerateResponseDataset(m, queryExpression)).ToList(); - - return new QueryWorkitemResourceResponse(workitemResponses, status); - } - - /// - /// Includes workitem attributes as specified in - /// . - /// - private static DicomDataset GenerateResponseDataset(DicomDataset dicomDataset, BaseQueryExpression queryExpression) - { - EnsureArg.IsNotNull(dicomDataset, nameof(dicomDataset)); - EnsureArg.IsNotNull(queryExpression, nameof(queryExpression)); - - // Should never be returned - dicomDataset = dicomDataset.Remove(DicomTag.TransactionUID); - - if (queryExpression.IncludeFields.All) - { - return dicomDataset; - } - - var tagsToReturn = new HashSet(RequiredReturnTags); - - foreach (DicomTag tag in queryExpression.IncludeFields.DicomTags) - { - tagsToReturn.Add(tag); - } - - foreach (var cond in queryExpression.FilterConditions) - { - tagsToReturn.Add(cond.QueryTag.Tag); - } - - dicomDataset.Remove(di => !tagsToReturn.Any( - t => t.Group == di.Tag.Group && - t.Element == di.Tag.Element)); - - return dicomDataset; - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemQueryTagService.cs b/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemQueryTagService.cs deleted file mode 100644 index e1d2ade9ce..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemQueryTagService.cs +++ /dev/null @@ -1,66 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using FellowOakDicom; -using Microsoft.Extensions.Logging; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Features.Common; - -namespace Microsoft.Health.Dicom.Core.Features.Workitem; - -public sealed class WorkitemQueryTagService : IWorkitemQueryTagService, IDisposable -{ - private readonly IIndexWorkitemStore _indexWorkitemStore; - private readonly AsyncCache> _queryTagCache; - private readonly IDicomTagParser _dicomTagParser; - private readonly ILogger _logger; - - public WorkitemQueryTagService(IIndexWorkitemStore indexWorkitemStore, IDicomTagParser dicomTagParser, ILogger logger) - { - _indexWorkitemStore = EnsureArg.IsNotNull(indexWorkitemStore, nameof(indexWorkitemStore)); - _queryTagCache = new AsyncCache>(ResolveQueryTagsAsync); - _dicomTagParser = EnsureArg.IsNotNull(dicomTagParser, nameof(dicomTagParser)); - _logger = EnsureArg.IsNotNull(logger, nameof(logger)); - } - - public void Dispose() - { - _queryTagCache.Dispose(); - GC.SuppressFinalize(this); - } - - public async Task> GetQueryTagsAsync(CancellationToken cancellationToken = default) - { - return await _queryTagCache.GetAsync(cancellationToken: cancellationToken); - } - - private async Task> ResolveQueryTagsAsync(CancellationToken cancellationToken) - { - var workitemQueryTags = await _indexWorkitemStore.GetWorkitemQueryTagsAsync(cancellationToken); - - foreach (var tag in workitemQueryTags) - { - if (_dicomTagParser.TryParse(tag.Path, out DicomTag[] dicomTags, true)) - { - tag.PathTags = Array.AsReadOnly(dicomTags); - } - else - { - _logger.LogError("Failed to parse dicom path '{TagPath}' to dicom tags.", tag.Path); - throw new DataStoreException(DicomCoreResource.DataStoreOperationFailed); - } - } - - return workitemQueryTags.Select(x => new QueryTag(x)).ToList(); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemRequestValidatorExtensions.cs b/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemRequestValidatorExtensions.cs deleted file mode 100644 index 5b783467ba..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemRequestValidatorExtensions.cs +++ /dev/null @@ -1,82 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Validation; -using Microsoft.Health.Dicom.Core.Messages.Workitem; - -namespace Microsoft.Health.Dicom.Core.Features.Workitem; - -internal static class WorkitemRequestValidatorExtensions -{ - /// - /// Validates an . - /// - /// The request to validate. - /// Thrown when request body is missing. - /// Thrown when the specified WorkitemInstanceUID is not a valid identifier. - internal static void Validate(this AddWorkitemRequest request) - { - EnsureArg.IsNotNull(request, nameof(request)); - if (request.DicomDataset == null) - { - throw new BadRequestException(DicomCoreResource.MissingRequestBody); - } - - UidValidation.Validate(request.WorkitemInstanceUid, nameof(request.WorkitemInstanceUid), allowEmpty: true); - } - - /// - /// Validates an . - /// - /// The request to validate. - /// Thrown when request body is mising. - /// Thrown when the specified WorkitemInstanceUID is not a valid identifier. - internal static void Validate(this UpdateWorkitemRequest request) - { - EnsureArg.IsNotNull(request, nameof(request)); - if (request.DicomDataset == null) - { - throw new BadRequestException(DicomCoreResource.MissingRequestBody); - } - - UidValidation.Validate(request.WorkitemInstanceUid, nameof(request.WorkitemInstanceUid), allowEmpty: false); - - // Transaction UID can be empty if workitem is in SCHEDULED state. - UidValidation.Validate(request.TransactionUid, nameof(request.TransactionUid), allowEmpty: true); - } - - internal static void Validate(this CancelWorkitemRequest request) - { - EnsureArg.IsNotNull(request, nameof(request)); - - if (request.DicomDataset == null) - { - throw new BadRequestException(DicomCoreResource.MissingRequestBody); - } - - UidValidation.Validate(request.WorkitemInstanceUid, nameof(request.WorkitemInstanceUid), allowEmpty: false); - } - - internal static void Validate(this ChangeWorkitemStateRequest request) - { - EnsureArg.IsNotNull(request, nameof(request)); - - if (request.DicomDataset == null) - { - throw new BadRequestException(DicomCoreResource.MissingRequestBody); - } - - UidValidation.Validate(request.WorkitemInstanceUid, nameof(request.WorkitemInstanceUid), allowEmpty: false); - } - - internal static void Validate(this RetrieveWorkitemRequest request) - { - EnsureArg.IsNotNull(request, nameof(request)); - - UidValidation.Validate(request.WorkitemInstanceUid, nameof(request.WorkitemInstanceUid), allowEmpty: false); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemResponseBuilder.cs b/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemResponseBuilder.cs deleted file mode 100644 index 6e161b3bf9..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemResponseBuilder.cs +++ /dev/null @@ -1,170 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using FellowOakDicom; -using EnsureThat; -using Microsoft.Health.Dicom.Core.Features.Routing; -using Microsoft.Health.Dicom.Core.Messages.Workitem; -using Microsoft.Health.Dicom.Core.Features.Store; -using System.Linq; - -namespace Microsoft.Health.Dicom.Core.Features.Workitem; - -/// -/// Provides functionality to build the response for the store transaction. -/// -public class WorkitemResponseBuilder : IWorkitemResponseBuilder -{ - private readonly static ushort[] WorkitemConflictFailureReasonCodes = new[] - { - FailureReasonCodes.UpsInstanceUpdateNotAllowed, - FailureReasonCodes.UpsPerformerChoosesNotToCancel, - FailureReasonCodes.UpsIsAlreadyCanceled, - FailureReasonCodes.UpsIsAlreadyCompleted - }; - - private readonly IUrlResolver _urlResolver; - private DicomDataset _dataset; - private string _message; - - public WorkitemResponseBuilder(IUrlResolver urlResolver) - { - EnsureArg.IsNotNull(urlResolver, nameof(urlResolver)); - - _urlResolver = urlResolver; - } - - /// - public AddWorkitemResponse BuildAddResponse() - { - Uri url = null; - WorkitemResponseStatus status = WorkitemResponseStatus.Failure; - - if (!_dataset.TryGetSingleValue(DicomTag.FailureReason, out var failureReason)) - { - status = WorkitemResponseStatus.Success; - url = _urlResolver.ResolveRetrieveWorkitemUri(_dataset.GetString(DicomTag.SOPInstanceUID)); - } - else if (failureReason == FailureReasonCodes.SopInstanceAlreadyExists) - { - status = WorkitemResponseStatus.Conflict; - } - - return new AddWorkitemResponse(status, url, _message); - } - - /// - public CancelWorkitemResponse BuildCancelResponse() - { - var status = WorkitemResponseStatus.Failure; - - if (!_dataset.TryGetSingleValue(DicomTag.FailureReason, out var failureReason)) - { - status = WorkitemResponseStatus.Success; - } - else if (WorkitemConflictFailureReasonCodes.Contains(failureReason)) - { - status = WorkitemResponseStatus.Conflict; - } - else if (failureReason == FailureReasonCodes.UpsInstanceNotFound) - { - status = WorkitemResponseStatus.NotFound; - } - - return new CancelWorkitemResponse(status, _message); - } - - public ChangeWorkitemStateResponse BuildChangeWorkitemStateResponse() - { - var status = WorkitemResponseStatus.Failure; - - if (!_dataset.TryGetSingleValue(DicomTag.FailureReason, out var failureReason)) - { - status = WorkitemResponseStatus.Success; - } - else if (failureReason == FailureReasonCodes.ValidationFailure) - { - status = WorkitemResponseStatus.Failure; - } - else if (failureReason == FailureReasonCodes.UpsInstanceNotFound) - { - status = WorkitemResponseStatus.NotFound; - } - else if (failureReason == FailureReasonCodes.UpsInstanceUpdateNotAllowed) - { - status = WorkitemResponseStatus.Conflict; - } - - return new ChangeWorkitemStateResponse(status, _message); - } - - /// - public RetrieveWorkitemResponse BuildRetrieveWorkitemResponse() - { - var status = WorkitemResponseStatus.Failure; - - if (!_dataset.TryGetSingleValue(DicomTag.FailureReason, out var failureReason)) - { - status = WorkitemResponseStatus.Success; - } - else if (failureReason == FailureReasonCodes.UpsInstanceNotFound) - { - status = WorkitemResponseStatus.NotFound; - } - - // always remove Transaction UID from the result dicomDataset. - if (null != _dataset) - { - _dataset.Remove(DicomTag.TransactionUID); - } - - return new RetrieveWorkitemResponse(status, _dataset, _message); - } - - /// - public UpdateWorkitemResponse BuildUpdateWorkitemResponse(string workitemInstanceUid = null) - { - Uri url = null; - WorkitemResponseStatus status = WorkitemResponseStatus.Failure; - - if (!_dataset.TryGetSingleValue(DicomTag.FailureReason, out var failureReason) - && !string.IsNullOrWhiteSpace(workitemInstanceUid)) - { - status = WorkitemResponseStatus.Success; - url = _urlResolver.ResolveRetrieveWorkitemUri(workitemInstanceUid); - } - else if (failureReason == FailureReasonCodes.UpsUpdateConflict) - { - status = WorkitemResponseStatus.Conflict; - } - - return new UpdateWorkitemResponse(status, url, _message); - } - - /// - public void AddSuccess(DicomDataset dicomDataset) - { - EnsureArg.IsNotNull(dicomDataset, nameof(dicomDataset)); - - _dataset = dicomDataset; - } - - /// - public void AddSuccess(string warning = default) - { - _dataset = new DicomDataset(); - _message = warning ?? string.Empty; - } - - /// - public void AddFailure(ushort? failureReasonCode, string message = null, DicomDataset dicomDataset = null) - { - _message = message; - _dataset = dicomDataset ?? new DicomDataset(); - - _dataset.Add(DicomTag.FailureReason, failureReasonCode.GetValueOrDefault(FailureReasonCodes.ProcessingFailure)); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemService.Add.cs b/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemService.Add.cs deleted file mode 100644 index 4e36c2362b..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemService.Add.cs +++ /dev/null @@ -1,137 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using FellowOakDicom; -using Microsoft.Extensions.Logging; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Store; -using Microsoft.Health.Dicom.Core.Features.Store.Entries; -using Microsoft.Health.Dicom.Core.Messages.Workitem; - -namespace Microsoft.Health.Dicom.Core.Features.Workitem; - -/// -/// Provides functionality to process the list of . -/// -public partial class WorkitemService -{ - private const string WorklistLabel = "worklist"; - - public async Task ProcessAddAsync(DicomDataset dataset, string workitemInstanceUid, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(dataset, nameof(dataset)); - - SetSpecifiedAttributesForCreate(dataset, workitemInstanceUid); - - if (ValidateAddRequest(dataset)) - { - await AddWorkitemAsync(dataset, cancellationToken).ConfigureAwait(false); - } - - return _responseBuilder.BuildAddResponse(); - } - - /// - /// Sets attributes that are the Service Class Provider's responsibility according to: - /// - /// - /// Dicom dataset. - /// Workitem query parameter. - internal static void SetSpecifiedAttributesForCreate(DicomDataset dataset, string workitemQueryParameter) - { - // SOP Common Module - dataset.AddOrUpdate(DicomTag.SOPClassUID, DicomUID.UnifiedProcedureStepPush); - ReconcileWorkitemInstanceUid(dataset, workitemQueryParameter); - - // Unified Procedure Step Scheduled Procedure Information Module - dataset.AddOrUpdate(DicomTag.ScheduledProcedureStepModificationDateTime, DateTime.UtcNow); - dataset.AddOrUpdate(DicomTag.WorklistLabel, WorklistLabel); - - // Unified Procedure Step Progress Information Module - dataset.AddOrUpdate(DicomTag.ProcedureStepState, ProcedureStepState.Scheduled); - } - - /// - /// Sets the dataset value from the query parameter as long as there is no conflict. - /// - internal static void ReconcileWorkitemInstanceUid(DicomDataset dataset, string workitemQueryParameter) - { - if (!string.IsNullOrWhiteSpace(workitemQueryParameter)) - { - var uidInDataset = dataset.TryGetString(DicomTag.SOPInstanceUID, out var sopInstanceUid); - - if (uidInDataset && !string.Equals(workitemQueryParameter, sopInstanceUid, StringComparison.Ordinal)) - { - throw new DatasetValidationException( - FailureReasonCodes.ValidationFailure, - DicomCoreResource.MismatchSopInstanceWorkitemInstanceUid); - } - - dataset.AddOrUpdate(DicomTag.SOPInstanceUID, workitemQueryParameter); - } - } - - private bool ValidateAddRequest(DicomDataset dataset) - { - try - { - GetValidator().Validate(dataset); - return true; - } - catch (Exception ex) - { - ushort failureCode = FailureReasonCodes.ProcessingFailure; - - switch (ex) - { - case DatasetValidationException dicomDatasetValidationException: - failureCode = dicomDatasetValidationException.FailureCode; - break; - - case ValidationException _: - failureCode = FailureReasonCodes.ValidationFailure; - break; - } - - _logger.LogInformation(ex, "Validation failed for the DICOM instance work-item entry. Failure code: {FailureCode}.", failureCode); - - _responseBuilder.AddFailure(failureCode, ex.Message, dataset); - - return false; - } - } - - private async Task AddWorkitemAsync(DicomDataset dataset, CancellationToken cancellationToken) - { - try - { - await _workitemOrchestrator.AddWorkitemAsync(dataset, cancellationToken).ConfigureAwait(false); - - _logger.LogInformation("Successfully added the DICOM instance work-item entry."); - - _responseBuilder.AddSuccess(dataset); - } - catch (Exception ex) - { - ushort failureCode = FailureReasonCodes.ProcessingFailure; - - switch (ex) - { - case WorkitemAlreadyExistsException _: - failureCode = FailureReasonCodes.SopInstanceAlreadyExists; - break; - } - - _logger.LogWarning(ex, "Failed to add the DICOM instance work-item entry. Failure code: {FailureCode}.", failureCode); - - // TODO: This can return the Database Error as is. We need to abstract that detail. - _responseBuilder.AddFailure(failureCode, ex.Message, dataset); - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemService.Cancel.cs b/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemService.Cancel.cs deleted file mode 100644 index 4404c02fc3..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemService.Cancel.cs +++ /dev/null @@ -1,240 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Globalization; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using FellowOakDicom; -using Microsoft.Extensions.Logging; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Store; -using Microsoft.Health.Dicom.Core.Features.Store.Entries; -using Microsoft.Health.Dicom.Core.Features.Workitem.Model; -using Microsoft.Health.Dicom.Core.Messages.Workitem; - -namespace Microsoft.Health.Dicom.Core.Features.Workitem; - -/// -/// Provides functionality to process the list of . -/// -public partial class WorkitemService -{ - public async Task ProcessCancelAsync( - DicomDataset dataset, - string workitemInstanceUid, - CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(dataset, nameof(dataset)); - EnsureArg.IsNotEmptyOrWhiteSpace(workitemInstanceUid, nameof(workitemInstanceUid)); - - var workitemMetadata = await _workitemOrchestrator - .GetWorkitemMetadataAsync(workitemInstanceUid, cancellationToken) - .ConfigureAwait(false); - - if (workitemMetadata == null) - { - _responseBuilder.AddFailure( - FailureReasonCodes.UpsInstanceNotFound, - DicomCoreResource.WorkitemInstanceNotFound, - dataset); - return _responseBuilder.BuildCancelResponse(); - } - - // Get the state transition result - var transitionStateResult = workitemMetadata - .ProcedureStepState - .GetTransitionState(WorkitemActionEvent.NActionToRequestCancel); - var cancelRequestDataset = await GetPreparedRequestCancelWorkitemBlobDatasetAsync( - dataset, workitemMetadata, transitionStateResult.State, cancellationToken) - .ConfigureAwait(false); - - await ValidateAndCancelWorkitemAsync( - cancelRequestDataset, - workitemInstanceUid, - workitemMetadata, - transitionStateResult, - cancellationToken) - .ConfigureAwait(false); - - return _responseBuilder.BuildCancelResponse(); - } - - private async Task ValidateAndCancelWorkitemAsync( - DicomDataset cancelRequestDataset, - string workitemInstanceUid, - WorkitemMetadataStoreEntry workitemMetadata, - WorkitemStateTransitionResult transitionStateResult, - CancellationToken cancellationToken) - { - if (ValidateCancelRequest(workitemInstanceUid, workitemMetadata, cancelRequestDataset, transitionStateResult)) - { - // If there is a warning code, the workitem is already in the canceled state. - if (transitionStateResult.HasWarningWithCode) - { - _responseBuilder.AddSuccess( - string.Format( - CultureInfo.InvariantCulture, - DicomCoreResource.WorkitemIsInFinalState, - workitemMetadata.ProcedureStepStateStringValue, - transitionStateResult.Code)); - } - else - { - await CancelWorkitemAsync( - cancelRequestDataset, - workitemMetadata, - transitionStateResult.State, - cancellationToken) - .ConfigureAwait(false); - } - } - } - - private async Task GetPreparedRequestCancelWorkitemBlobDatasetAsync( - DicomDataset dataset, - WorkitemMetadataStoreEntry workitemMetadata, - ProcedureStepState targetProcedureStepState, - CancellationToken cancellationToken) - { - try - { - // Get the workitem from blob store - var workitemDataset = await _workitemOrchestrator - .GetWorkitemBlobAsync(workitemMetadata, cancellationToken) - .ConfigureAwait(false); - - PopulateCancelRequestAttributes(workitemDataset, dataset, targetProcedureStepState); - - return workitemDataset; - } - catch (Exception ex) - { - _logger.LogError(ex, @"Error while preparing Cancel Request Blob Dataset"); - - throw; - } - } - - private static DicomDataset PopulateCancelRequestAttributes( - DicomDataset workitemDataset, - DicomDataset cancelRequestDataset, - ProcedureStepState procedureStepState) - { - workitemDataset.AddOrUpdate(DicomTag.ProcedureStepCancellationDateTime, DateTime.UtcNow); - workitemDataset.AddOrUpdate(DicomTag.ProcedureStepState, procedureStepState.GetStringValue()); - - var cancellationReason = cancelRequestDataset.GetSingleValueOrDefault(DicomTag.ReasonForCancellation, string.Empty); - var discontinuationReasonCodeSequence = new DicomSequence(DicomTag.ProcedureStepDiscontinuationReasonCodeSequence, new DicomDataset - { - { DicomTag.ReasonForCancellation, cancellationReason } - }); - workitemDataset.AddOrUpdate(discontinuationReasonCodeSequence); - - var progressInformationSequence = new DicomSequence(DicomTag.ProcedureStepProgressInformationSequence, new DicomDataset - { - { DicomTag.ProcedureStepCancellationDateTime, DateTime.UtcNow }, - new DicomSequence(DicomTag.ProcedureStepDiscontinuationReasonCodeSequence, new DicomDataset - { - { DicomTag.ReasonForCancellation, cancellationReason } - }), - new DicomSequence(DicomTag.ProcedureStepCommunicationsURISequence, new DicomDataset - { - { DicomTag.ContactURI, cancelRequestDataset.GetSingleValueOrDefault(DicomTag.ContactURI, string.Empty) }, - { DicomTag.ContactDisplayName, cancelRequestDataset.GetSingleValueOrDefault(DicomTag.ContactDisplayName, string.Empty) }, - }) - }); - workitemDataset.AddOrUpdate(progressInformationSequence); - - // TODO: Remove this once Update workitem feature is implemented - // This is a workaround for Cancel workitem to work without Update workitem - if (cancelRequestDataset.TryGetSequence(DicomTag.UnifiedProcedureStepPerformedProcedureSequence, out var unifiedProcedureStepPerformedProcedureSequence)) - { - workitemDataset.AddOrUpdate(unifiedProcedureStepPerformedProcedureSequence); - } - - return workitemDataset; - } - - private bool ValidateCancelRequest( - string workitemInstanceUid, - WorkitemMetadataStoreEntry workitemMetadata, - DicomDataset dataset, - WorkitemStateTransitionResult transitionStateResult) - { - try - { - CancelWorkitemDatasetValidator.ValidateWorkitemState( - workitemInstanceUid, - workitemMetadata, - transitionStateResult); - - GetValidator().Validate(dataset); - - return true; - } - catch (Exception ex) - { - ushort? failureCode = FailureReasonCodes.ProcessingFailure; - - switch (ex) - { - case DatasetValidationException datasetValidationException: - failureCode = datasetValidationException.FailureCode; - break; - - case DicomValidationException _: - case ValidationException _: - failureCode = FailureReasonCodes.UpsInstanceUpdateNotAllowed; - break; - - case WorkitemNotFoundException: - failureCode = FailureReasonCodes.UpsInstanceNotFound; - break; - } - - _logger.LogInformation(ex, - "Validation failed for the DICOM instance work-item entry. Failure code: {FailureCode}.", failureCode); - - _responseBuilder.AddFailure(failureCode, ex.Message, dataset); - - return false; - } - } - - private async Task CancelWorkitemAsync( - DicomDataset dataset, - WorkitemMetadataStoreEntry workitemMetadata, - ProcedureStepState targetProcedureStepState, - CancellationToken cancellationToken) - { - try - { - await _workitemOrchestrator - .UpdateWorkitemStateAsync(dataset, workitemMetadata, targetProcedureStepState, cancellationToken) - .ConfigureAwait(false); - - _logger.LogInformation("Successfully canceled the work-item entry."); - - _responseBuilder.AddSuccess(DicomCoreResource.WorkitemCancelRequestSuccess); - } - catch (Exception ex) - { - ushort failureCode = FailureReasonCodes.ProcessingFailure; - - switch (ex) - { - case WorkitemNotFoundException _: - failureCode = FailureReasonCodes.ProcessingFailure; - break; - } - - _logger.LogWarning(ex, "Failed to cancel the work-item entry. Failure code: {FailureCode}.", failureCode); - - _responseBuilder.AddFailure(failureCode, ex.Message, dataset); - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemService.ChangeState.cs b/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemService.ChangeState.cs deleted file mode 100644 index ab737086f6..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemService.ChangeState.cs +++ /dev/null @@ -1,127 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Globalization; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using FellowOakDicom; -using Microsoft.Extensions.Logging; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Store; -using Microsoft.Health.Dicom.Core.Features.Store.Entries; -using Microsoft.Health.Dicom.Core.Features.Workitem.Model; -using Microsoft.Health.Dicom.Core.Messages.Workitem; - -namespace Microsoft.Health.Dicom.Core.Features.Workitem; - -/// -/// Provides functionality to process the list of . -/// -public partial class WorkitemService -{ - public async Task ProcessChangeStateAsync( - DicomDataset dataset, - string workitemInstanceUid, - CancellationToken cancellationToken = default) - { - EnsureArg.IsNotNull(dataset, nameof(dataset)); - EnsureArg.IsNotEmptyOrWhiteSpace(workitemInstanceUid, nameof(workitemInstanceUid)); - - try - { - GetValidator().Validate(dataset); - - var workitemMetadata = await _workitemOrchestrator - .GetWorkitemMetadataAsync(workitemInstanceUid, cancellationToken) - .ConfigureAwait(false); - if (workitemMetadata == null) - { - _responseBuilder.AddFailure( - FailureReasonCodes.UpsInstanceNotFound, - DicomCoreResource.WorkitemInstanceNotFound, - dataset); - - return _responseBuilder.BuildChangeWorkitemStateResponse(); - } - - var transitionStateResult = ValidateChangeWorkitemStateRequest(dataset, workitemMetadata); - var originalBlobDicomDataset = await _workitemOrchestrator - .GetWorkitemBlobAsync(workitemMetadata, cancellationToken) - .ConfigureAwait(false); - - var updateDataset = GetPreparedChangeWorkitemStateDicomDataset(dataset, originalBlobDicomDataset); - var targetProcedureStepState = updateDataset.GetProcedureStepState(); - - updateDataset.ValidateFinalStateRequirement(); - - if (transitionStateResult.HasWarningWithCode) - { - _responseBuilder.AddSuccess( - string.Format( - CultureInfo.InvariantCulture, - DicomCoreResource.WorkitemIsInFinalState, - workitemMetadata.ProcedureStepStateStringValue, - transitionStateResult.Code)); - } - else - { - await _workitemOrchestrator.UpdateWorkitemStateAsync( - updateDataset, - workitemMetadata, - dataset.GetProcedureStepState(), - cancellationToken) - .ConfigureAwait(false); - - _responseBuilder.AddSuccess(string.Empty); - } - } - catch (Exception ex) - { - ushort? failureCode = FailureReasonCodes.ProcessingFailure; - switch (ex) - { - case BadRequestException: - case DicomValidationException: - case DatasetValidationException: - case ValidationException: - failureCode = FailureReasonCodes.ValidationFailure; - break; - - case WorkitemUpdateNotAllowedException: - failureCode = FailureReasonCodes.UpsInstanceUpdateNotAllowed; - break; - - case ItemNotFoundException: - failureCode = FailureReasonCodes.UpsInstanceNotFound; - break; - } - - _logger.LogInformation(ex, - "Change workitem state failed for the DICOM instance work-item entry. Failure code: {FailureCode}.", failureCode); - - _responseBuilder.AddFailure(failureCode, ex.Message); - } - - return _responseBuilder.BuildChangeWorkitemStateResponse(); - } - - private static DicomDataset GetPreparedChangeWorkitemStateDicomDataset(DicomDataset dataset, DicomDataset originalBlobDicomDataset) - { - var resultDataset = originalBlobDicomDataset - .AddOrUpdate(DicomTag.TransactionUID, dataset.GetString(DicomTag.TransactionUID)) - .AddOrUpdate(DicomTag.ProcedureStepState, dataset.GetString(DicomTag.ProcedureStepState)); - - return resultDataset; - } - - private static WorkitemStateTransitionResult ValidateChangeWorkitemStateRequest(DicomDataset dataset, WorkitemMetadataStoreEntry workitemMetadata) - { - EnsureArg.IsNotNull(workitemMetadata, nameof(workitemMetadata)); - - return ChangeWorkitemStateDatasetValidator.ValidateWorkitemState(dataset, workitemMetadata); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemService.Query.cs b/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemService.Query.cs deleted file mode 100644 index e8fb8e987a..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemService.Query.cs +++ /dev/null @@ -1,44 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.Extensions.Logging; -using Microsoft.Health.Dicom.Core.Features.Query; -using Microsoft.Health.Dicom.Core.Features.Store; -using Microsoft.Health.Dicom.Core.Features.Store.Entries; -using Microsoft.Health.Dicom.Core.Messages.Workitem; - -namespace Microsoft.Health.Dicom.Core.Features.Workitem; - -/// -/// Provides functionality to process the list of . -/// -public partial class WorkitemService -{ - public async Task ProcessQueryAsync(BaseQueryParameters parameters, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(parameters, nameof(parameters)); - - try - { - var result = await _workitemOrchestrator.QueryAsync(parameters, cancellationToken).ConfigureAwait(false); - - _logger.LogInformation("Successfully queried the DICOM instance work-item entry."); - - return result; - } - catch (Exception ex) - { - ushort failureCode = FailureReasonCodes.ProcessingFailure; - - _logger.LogWarning(ex, "Failed to query the DICOM instance work-item entry. Failure code: {FailureCode}.", failureCode); - - throw; - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemService.Retrieve.cs b/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemService.Retrieve.cs deleted file mode 100644 index e56a7564b9..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemService.Retrieve.cs +++ /dev/null @@ -1,68 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.Extensions.Logging; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Store; -using Microsoft.Health.Dicom.Core.Messages.Workitem; - -namespace Microsoft.Health.Dicom.Core.Features.Workitem; - -/// -/// Provides functionality to process the search request based on the Workitem Instance UID. -/// -public partial class WorkitemService -{ - /// - public async Task ProcessRetrieveAsync(string workitemInstanceUid, CancellationToken cancellationToken) - { - EnsureArg.IsNotEmptyOrWhiteSpace(workitemInstanceUid, nameof(workitemInstanceUid)); - - try - { - var workitemMetadata = await _workitemOrchestrator - .GetWorkitemMetadataAsync(workitemInstanceUid, cancellationToken) - .ConfigureAwait(false); - - if (workitemMetadata == null) - { - _responseBuilder.AddFailure( - FailureReasonCodes.UpsInstanceNotFound, - DicomCoreResource.WorkitemInstanceNotFound); - - return _responseBuilder.BuildRetrieveWorkitemResponse(); - } - - var dicomDataset = await _workitemOrchestrator - .RetrieveWorkitemAsync(workitemMetadata, cancellationToken) - .ConfigureAwait(false); - - _responseBuilder.AddSuccess(dicomDataset); - - _logger.LogInformation("Successfully retrieved the DICOM instance work-item entry."); - } - catch (Exception ex) - { - ushort failureCode = FailureReasonCodes.ProcessingFailure; - - _logger.LogWarning(ex, "Failed to retrieve the DICOM instance work-item entry. Failure code: {FailureCode}.", failureCode); - switch (ex) - { - case DataStoreException: - case ItemNotFoundException: - failureCode = FailureReasonCodes.UpsInstanceNotFound; - break; - } - - _responseBuilder.AddFailure(failureCode, ex.Message); - } - - return _responseBuilder.BuildRetrieveWorkitemResponse(); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemService.Update.cs b/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemService.Update.cs deleted file mode 100644 index 784a5120dc..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemService.Update.cs +++ /dev/null @@ -1,180 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Globalization; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using FellowOakDicom; -using Microsoft.Extensions.Logging; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Store; -using Microsoft.Health.Dicom.Core.Features.Store.Entries; -using Microsoft.Health.Dicom.Core.Features.Workitem.Model; -using Microsoft.Health.Dicom.Core.Messages.Workitem; - -namespace Microsoft.Health.Dicom.Core.Features.Workitem; - -/// -/// Provides functionality to process the list of . -/// -public partial class WorkitemService -{ - public async Task ProcessUpdateAsync(DicomDataset dataset, string workitemInstanceUid, string transactionUid, CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(dataset, nameof(dataset)); - EnsureArg.IsNotEmptyOrWhiteSpace(workitemInstanceUid, nameof(workitemInstanceUid)); - - WorkitemMetadataStoreEntry workitemMetadata = await _workitemOrchestrator - .GetWorkitemMetadataAsync(workitemInstanceUid, cancellationToken) - .ConfigureAwait(false); - - // If workitem metadata is not found in SQL DB, return UpsInstanceNotFound failure. - if (workitemMetadata == null) - { - _responseBuilder.AddFailure( - FailureReasonCodes.UpsInstanceNotFound, - string.Format(CultureInfo.InvariantCulture, DicomCoreResource.InvalidWorkitemInstanceTargetUri, workitemInstanceUid), - dataset); - return _responseBuilder.BuildUpdateWorkitemResponse(); - } - - SetSpecifiedAttributesForUpdate(dataset); - - // Validate the following: - // 1. If state is SCHEDULED, transaction UID is not provided. - // 2. If state is IN PROGRESS, provided transaction UID matches the existing transaction UID. - // 3. State is not COMPLETED or CANCELED. - // 4. Dataset is valid. - if (ValidateUpdateRequest(dataset, workitemMetadata, transactionUid)) - { - await UpdateWorkitemAsync(dataset, workitemMetadata, cancellationToken) - .ConfigureAwait(false); - } - - return _responseBuilder.BuildUpdateWorkitemResponse(workitemInstanceUid); - } - - /// - /// Sets attributes that are the Service Class Provider's responsibility according to: - /// - /// - /// Dicom dataset. - internal static void SetSpecifiedAttributesForUpdate(DicomDataset dataset) - { - // Set Scheduled Procedure Step Modification DateTime as the current time. - // Reference: https://dicom.nema.org/medical/dicom/current/output/html/part04.html#table_CC.2.5-3 - dataset.AddOrUpdate(DicomTag.ScheduledProcedureStepModificationDateTime, DateTime.UtcNow); - } - - /// - /// Validate the following: - /// 1. If state is SCHEDULED, transaction UID is not provided. - /// 2. If state is IN PROGRESS, provided transaction UID matches the existing transaction UID. - /// 3. State is not COMPLETED or CANCELED. - /// 4. Dataset is valid. - /// - /// Incoming dataset to be validated. - /// Workitem metadata. - /// Transaction UID. - /// True if validated, else return false. - private bool ValidateUpdateRequest(DicomDataset dataset, WorkitemMetadataStoreEntry workitemMetadata, string transactionUid) - { - try - { - UpdateWorkitemDatasetValidator.ValidateWorkitemStateAndTransactionUid(transactionUid, workitemMetadata); - - GetValidator().Validate(dataset); - - return true; - } - catch (Exception ex) - { - ushort failureCode = FailureReasonCodes.ProcessingFailure; - - switch (ex) - { - case DatasetValidationException dicomDatasetValidationException: - failureCode = dicomDatasetValidationException.FailureCode; - break; - - case DicomValidationException: - case ValidationException: - failureCode = FailureReasonCodes.ValidationFailure; - break; - - case WorkitemNotFoundException: - failureCode = FailureReasonCodes.UpsInstanceNotFound; - break; - } - - _logger.LogInformation(ex, "Validation failed for the DICOM instance work-item entry. Failure code: {FailureCode}.", failureCode); - - _responseBuilder.AddFailure(failureCode, ex.Message, dataset); - - return false; - } - } - - private async Task UpdateWorkitemAsync( - DicomDataset dataset, - WorkitemMetadataStoreEntry workitemMetadata, - CancellationToken cancellationToken) - { - try - { - // Retrieve existing dataset. - var existingDataset = await _workitemOrchestrator.RetrieveWorkitemAsync(workitemMetadata, cancellationToken) - .ConfigureAwait(false); - if (existingDataset == null) - { - _responseBuilder.AddFailure( - FailureReasonCodes.UpsInstanceNotFound, - DicomCoreResource.WorkitemInstanceNotFound, - dataset); - - return; - } - - // Update `existingDatabase` object. - DicomDataset updatedDataset = GetMergedDataset(existingDataset, dataset); - - await _workitemOrchestrator - .UpdateWorkitemAsync(updatedDataset, workitemMetadata, cancellationToken) - .ConfigureAwait(false); - - _logger.LogInformation("Successfully updated the DICOM instance work-item entry."); - - _responseBuilder.AddSuccess(); - } - catch (Exception ex) - { - ushort failureCode = FailureReasonCodes.ProcessingFailure; - - switch (ex) - { - case DataStoreException dsEx when dsEx.FailureCode.HasValue: - failureCode = FailureReasonCodes.UpsUpdateConflict; - break; - } - - _logger.LogWarning(ex, "Failed to update the DICOM instance work-item entry. Failure code: {FailureCode}.", failureCode); - - _responseBuilder.AddFailure(failureCode, ex.Message, dataset); - } - } - - private static DicomDataset GetMergedDataset(DicomDataset existingDataset, DicomDataset newDataset) - { - DicomDataset mergedDataset = existingDataset; - - newDataset.Each(di => mergedDataset.AddOrUpdate(newDataset, di.Tag, out mergedDataset)); - - return mergedDataset; - } - -} diff --git a/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemService.cs b/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemService.cs deleted file mode 100644 index 13e0864f25..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Features/Workitem/WorkitemService.cs +++ /dev/null @@ -1,41 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using EnsureThat; -using Microsoft.Extensions.Logging; - -namespace Microsoft.Health.Dicom.Core.Features.Workitem; - -[SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Response builder handles all exceptions.")] -public partial class WorkitemService : IWorkitemService -{ - private readonly IWorkitemResponseBuilder _responseBuilder; - private readonly IEnumerable _validators; - private readonly IWorkitemOrchestrator _workitemOrchestrator; - private readonly ILogger _logger; - - public WorkitemService( - IWorkitemResponseBuilder responseBuilder, - IEnumerable dicomDatasetValidators, - IWorkitemOrchestrator workitemOrchestrator, - ILogger logger) - { - _responseBuilder = EnsureArg.IsNotNull(responseBuilder, nameof(responseBuilder)); - _validators = EnsureArg.IsNotNull(dicomDatasetValidators, nameof(dicomDatasetValidators)); - _workitemOrchestrator = EnsureArg.IsNotNull(workitemOrchestrator, nameof(workitemOrchestrator)); - _logger = EnsureArg.IsNotNull(logger, nameof(logger)); - } - - private IWorkitemDatasetValidator GetValidator() where T : IWorkitemDatasetValidator - { - var validator = _validators.FirstOrDefault(o => string.Equals(o.Name, typeof(T).Name, StringComparison.Ordinal)); - - return validator; - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/ChangeFeed/ChangeFeedLatestRequest.cs b/src/Microsoft.Health.Dicom.Core/Messages/ChangeFeed/ChangeFeedLatestRequest.cs deleted file mode 100644 index 4c06dfb736..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/ChangeFeed/ChangeFeedLatestRequest.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using MediatR; -using Microsoft.Health.Dicom.Core.Features.ChangeFeed; - -namespace Microsoft.Health.Dicom.Core.Messages.ChangeFeed; - -public class ChangeFeedLatestRequest : IRequest -{ - public ChangeFeedLatestRequest(ChangeFeedOrder order, bool includeMetadata) - { - Order = order; - IncludeMetadata = includeMetadata; - } - - public ChangeFeedOrder Order { get; } - - public bool IncludeMetadata { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/ChangeFeed/ChangeFeedLatestResponse.cs b/src/Microsoft.Health.Dicom.Core/Messages/ChangeFeed/ChangeFeedLatestResponse.cs deleted file mode 100644 index 947c507408..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/ChangeFeed/ChangeFeedLatestResponse.cs +++ /dev/null @@ -1,18 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Microsoft.Health.Dicom.Core.Features.ChangeFeed; - -namespace Microsoft.Health.Dicom.Core.Messages.ChangeFeed; - -public class ChangeFeedLatestResponse -{ - public ChangeFeedLatestResponse(ChangeFeedEntry entry) - { - Entry = entry; - } - - public ChangeFeedEntry Entry { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/ChangeFeed/ChangeFeedRequest.cs b/src/Microsoft.Health.Dicom.Core/Messages/ChangeFeed/ChangeFeedRequest.cs deleted file mode 100644 index 995b97f777..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/ChangeFeed/ChangeFeedRequest.cs +++ /dev/null @@ -1,32 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using MediatR; -using Microsoft.Health.Dicom.Core.Features.ChangeFeed; -using Microsoft.Health.Dicom.Core.Models; - -namespace Microsoft.Health.Dicom.Core.Messages.ChangeFeed; - -public class ChangeFeedRequest : IRequest -{ - public ChangeFeedRequest(TimeRange range, long offset, int limit, ChangeFeedOrder order, bool includeMetadata) - { - Range = range; - Offset = offset; - Limit = limit; - Order = order; - IncludeMetadata = includeMetadata; - } - - public TimeRange Range { get; } - - public int Limit { get; } - - public long Offset { get; } - - public ChangeFeedOrder Order { get; } - - public bool IncludeMetadata { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/ChangeFeed/ChangeFeedResponse.cs b/src/Microsoft.Health.Dicom.Core/Messages/ChangeFeed/ChangeFeedResponse.cs deleted file mode 100644 index 2f37577d9f..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/ChangeFeed/ChangeFeedResponse.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using EnsureThat; -using Microsoft.Health.Dicom.Core.Features.ChangeFeed; - -namespace Microsoft.Health.Dicom.Core.Messages.ChangeFeed; - -public class ChangeFeedResponse -{ - public ChangeFeedResponse(IReadOnlyCollection entries) - { - EnsureArg.IsNotNull(entries, nameof(entries)); - - Entries = entries; - } - - public IReadOnlyCollection Entries { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/Delete/DeleteResourcesRequest.cs b/src/Microsoft.Health.Dicom.Core/Messages/Delete/DeleteResourcesRequest.cs deleted file mode 100644 index 3885dc4a0a..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/Delete/DeleteResourcesRequest.cs +++ /dev/null @@ -1,40 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using MediatR; - -namespace Microsoft.Health.Dicom.Core.Messages.Delete; - -public class DeleteResourcesRequest : IRequest -{ - public DeleteResourcesRequest(string studyInstanceUid) - { - StudyInstanceUid = studyInstanceUid; - ResourceType = ResourceType.Study; - } - - public DeleteResourcesRequest(string studyInstanceUid, string seriesInstanceUid) - { - StudyInstanceUid = studyInstanceUid; - SeriesInstanceUid = seriesInstanceUid; - ResourceType = ResourceType.Series; - } - - public DeleteResourcesRequest(string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid) - { - StudyInstanceUid = studyInstanceUid; - SeriesInstanceUid = seriesInstanceUid; - SopInstanceUid = sopInstanceUid; - ResourceType = ResourceType.Instance; - } - - public ResourceType ResourceType { get; } - - public string StudyInstanceUid { get; } - - public string SeriesInstanceUid { get; } - - public string SopInstanceUid { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/Delete/DeleteResourcesResponse.cs b/src/Microsoft.Health.Dicom.Core/Messages/Delete/DeleteResourcesResponse.cs deleted file mode 100644 index b7c933a489..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/Delete/DeleteResourcesResponse.cs +++ /dev/null @@ -1,13 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Messages.Delete; - -public class DeleteResourcesResponse -{ - public DeleteResourcesResponse() - { - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/Export/ExportRequest.cs b/src/Microsoft.Health.Dicom.Core/Messages/Export/ExportRequest.cs deleted file mode 100644 index 0da8faaea5..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/Export/ExportRequest.cs +++ /dev/null @@ -1,20 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using MediatR; -using Microsoft.Health.Dicom.Core.Models.Export; - -namespace Microsoft.Health.Dicom.Core.Messages.Export; - -public sealed class ExportRequest : IRequest -{ - public ExportSpecification Specification { get; } - - public ExportRequest(ExportSpecification spec) - { - Specification = EnsureArg.IsNotNull(spec, nameof(spec)); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/Export/ExportResponse.cs b/src/Microsoft.Health.Dicom.Core/Messages/Export/ExportResponse.cs deleted file mode 100644 index a96953c325..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/Export/ExportResponse.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using Microsoft.Health.Operations; - -namespace Microsoft.Health.Dicom.Core.Messages.Export; - -public class ExportResponse -{ - public OperationReference Operation { get; } - - public ExportResponse(OperationReference operation) - => Operation = EnsureArg.IsNotNull(operation, nameof(operation)); -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/ExtendedQueryTag/AddExtendedQueryTagRequest.cs b/src/Microsoft.Health.Dicom.Core/Messages/ExtendedQueryTag/AddExtendedQueryTagRequest.cs deleted file mode 100644 index e99903348c..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/ExtendedQueryTag/AddExtendedQueryTagRequest.cs +++ /dev/null @@ -1,20 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using MediatR; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -namespace Microsoft.Health.Dicom.Core.Messages.ExtendedQueryTag; - -public class AddExtendedQueryTagRequest : IRequest -{ - public AddExtendedQueryTagRequest(IEnumerable extendedQueryTags) - { - ExtendedQueryTags = extendedQueryTags; - } - - public IEnumerable ExtendedQueryTags { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/ExtendedQueryTag/AddExtendedQueryTagResponse.cs b/src/Microsoft.Health.Dicom.Core/Messages/ExtendedQueryTag/AddExtendedQueryTagResponse.cs deleted file mode 100644 index 25ff4c6f26..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/ExtendedQueryTag/AddExtendedQueryTagResponse.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using Microsoft.Health.Operations; - -namespace Microsoft.Health.Dicom.Core.Messages.ExtendedQueryTag; - -public class AddExtendedQueryTagResponse -{ - public AddExtendedQueryTagResponse(OperationReference operationReference) - { - Operation = EnsureArg.IsNotNull(operationReference); - } - - public OperationReference Operation { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/ExtendedQueryTag/DeleteExtendedQueryTagRequest.cs b/src/Microsoft.Health.Dicom.Core/Messages/ExtendedQueryTag/DeleteExtendedQueryTagRequest.cs deleted file mode 100644 index 41bad28ca1..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/ExtendedQueryTag/DeleteExtendedQueryTagRequest.cs +++ /dev/null @@ -1,18 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using MediatR; - -namespace Microsoft.Health.Dicom.Core.Messages.ExtendedQueryTag; - -public class DeleteExtendedQueryTagRequest : IRequest -{ - public DeleteExtendedQueryTagRequest(string tagPath) - { - TagPath = tagPath; - } - - public string TagPath { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/ExtendedQueryTag/DeleteExtendedQueryTagResponse.cs b/src/Microsoft.Health.Dicom.Core/Messages/ExtendedQueryTag/DeleteExtendedQueryTagResponse.cs deleted file mode 100644 index 82a253aa8b..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/ExtendedQueryTag/DeleteExtendedQueryTagResponse.cs +++ /dev/null @@ -1,10 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Messages.ExtendedQueryTag; - -public class DeleteExtendedQueryTagResponse -{ -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/ExtendedQueryTag/GetExtendedQueryTagErrorsRequest.cs b/src/Microsoft.Health.Dicom.Core/Messages/ExtendedQueryTag/GetExtendedQueryTagErrorsRequest.cs deleted file mode 100644 index 24bd93d32a..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/ExtendedQueryTag/GetExtendedQueryTagErrorsRequest.cs +++ /dev/null @@ -1,25 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using MediatR; - -namespace Microsoft.Health.Dicom.Core.Messages.ExtendedQueryTag; - -public class GetExtendedQueryTagErrorsRequest : IRequest -{ - public GetExtendedQueryTagErrorsRequest(string path, int limit, long offset) - { - Path = EnsureArg.IsNotNullOrWhiteSpace(path, nameof(path)); - Limit = EnsureArg.IsInRange(limit, 1, 200, nameof(limit)); - Offset = EnsureArg.IsGte(offset, 0, nameof(offset)); - } - - public string Path { get; } - - public int Limit { get; } - - public long Offset { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/ExtendedQueryTag/GetExtendedQueryTagErrorsResponse.cs b/src/Microsoft.Health.Dicom.Core/Messages/ExtendedQueryTag/GetExtendedQueryTagErrorsResponse.cs deleted file mode 100644 index 4350a12814..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/ExtendedQueryTag/GetExtendedQueryTagErrorsResponse.cs +++ /dev/null @@ -1,20 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using EnsureThat; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -namespace Microsoft.Health.Dicom.Core.Messages.ExtendedQueryTag; - -public class GetExtendedQueryTagErrorsResponse -{ - public GetExtendedQueryTagErrorsResponse(IReadOnlyCollection extendedQueryTagErrors) - { - ExtendedQueryTagErrors = EnsureArg.IsNotNull(extendedQueryTagErrors); - } - - public IReadOnlyCollection ExtendedQueryTagErrors { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/ExtendedQueryTag/GetExtendedQueryTagRequest.cs b/src/Microsoft.Health.Dicom.Core/Messages/ExtendedQueryTag/GetExtendedQueryTagRequest.cs deleted file mode 100644 index 0a42bd4f78..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/ExtendedQueryTag/GetExtendedQueryTagRequest.cs +++ /dev/null @@ -1,21 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using MediatR; - -namespace Microsoft.Health.Dicom.Core.Messages.ExtendedQueryTag; - -public class GetExtendedQueryTagRequest : IRequest -{ - public GetExtendedQueryTagRequest(string extendedQueryTagPath) - { - ExtendedQueryTagPath = extendedQueryTagPath; - } - - /// - /// Path for the extended query tag that is requested. - /// - public string ExtendedQueryTagPath { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/ExtendedQueryTag/GetExtendedQueryTagResponse.cs b/src/Microsoft.Health.Dicom.Core/Messages/ExtendedQueryTag/GetExtendedQueryTagResponse.cs deleted file mode 100644 index 471d06ded0..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/ExtendedQueryTag/GetExtendedQueryTagResponse.cs +++ /dev/null @@ -1,18 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -namespace Microsoft.Health.Dicom.Core.Messages.ExtendedQueryTag; - -public class GetExtendedQueryTagResponse -{ - public GetExtendedQueryTagResponse(GetExtendedQueryTagEntry extendedQueryTagEntry) - { - ExtendedQueryTag = extendedQueryTagEntry; - } - - public GetExtendedQueryTagEntry ExtendedQueryTag { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/ExtendedQueryTag/GetExtendedQueryTagsRequest.cs b/src/Microsoft.Health.Dicom.Core/Messages/ExtendedQueryTag/GetExtendedQueryTagsRequest.cs deleted file mode 100644 index de83db3b28..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/ExtendedQueryTag/GetExtendedQueryTagsRequest.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using MediatR; - -namespace Microsoft.Health.Dicom.Core.Messages.ExtendedQueryTag; - -public class GetExtendedQueryTagsRequest : IRequest -{ - public GetExtendedQueryTagsRequest(int limit, long offset) - { - Limit = EnsureArg.IsInRange(limit, 1, 200, nameof(limit)); - Offset = EnsureArg.IsGte(offset, 0, nameof(offset)); - } - - public int Limit { get; } - - public long Offset { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/ExtendedQueryTag/GetExtendedQueryTagsResponse.cs b/src/Microsoft.Health.Dicom.Core/Messages/ExtendedQueryTag/GetExtendedQueryTagsResponse.cs deleted file mode 100644 index f2244e2559..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/ExtendedQueryTag/GetExtendedQueryTagsResponse.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -namespace Microsoft.Health.Dicom.Core.Messages.ExtendedQueryTag; - -public class GetExtendedQueryTagsResponse -{ - public GetExtendedQueryTagsResponse(IEnumerable extendedQueryTagEntries) - { - ExtendedQueryTags = extendedQueryTagEntries; - } - - public IEnumerable ExtendedQueryTags { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/ExtendedQueryTag/UpdateTagQueryStatusRequest.cs b/src/Microsoft.Health.Dicom.Core/Messages/ExtendedQueryTag/UpdateTagQueryStatusRequest.cs deleted file mode 100644 index 529bcfe804..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/ExtendedQueryTag/UpdateTagQueryStatusRequest.cs +++ /dev/null @@ -1,23 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using MediatR; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -namespace Microsoft.Health.Dicom.Core.Messages.ExtendedQueryTag; - -public class UpdateExtendedQueryTagRequest : IRequest -{ - public UpdateExtendedQueryTagRequest(string tagPath, UpdateExtendedQueryTagEntry newValue) - { - TagPath = EnsureArg.IsNotNull(tagPath, nameof(tagPath)); - NewValue = EnsureArg.IsNotNull(newValue, nameof(newValue)); - } - - public string TagPath { get; } - - public UpdateExtendedQueryTagEntry NewValue { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/ExtendedQueryTag/UpdateTagQueryStatusResponse.cs b/src/Microsoft.Health.Dicom.Core/Messages/ExtendedQueryTag/UpdateTagQueryStatusResponse.cs deleted file mode 100644 index 6e407a111f..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/ExtendedQueryTag/UpdateTagQueryStatusResponse.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; - -namespace Microsoft.Health.Dicom.Core.Messages.ExtendedQueryTag; - -public class UpdateExtendedQueryTagResponse -{ - public UpdateExtendedQueryTagResponse(GetExtendedQueryTagEntry tagEntry) - { - TagEntry = EnsureArg.IsNotNull(tagEntry, nameof(tagEntry)); - } - - public GetExtendedQueryTagEntry TagEntry { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/Operations/OperationStateRequest.cs b/src/Microsoft.Health.Dicom.Core/Messages/Operations/OperationStateRequest.cs deleted file mode 100644 index dafee6bd82..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/Operations/OperationStateRequest.cs +++ /dev/null @@ -1,29 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using MediatR; - -namespace Microsoft.Health.Dicom.Core.Messages.Operations; - -/// -/// Represents a request for the state of long-running DICOM operations. -/// -public class OperationStateRequest : IRequest -{ - /// - /// Initializes a new instance of the class. - /// - /// The unique ID for a particular DICOM operation. - public OperationStateRequest(Guid operationId) - => OperationId = operationId; - - /// - /// Gets the operation ID. - /// - /// The unique ID that denotes a particular operation. - public Guid OperationId { get; } -} - diff --git a/src/Microsoft.Health.Dicom.Core/Messages/Operations/OperationStateResponse.cs b/src/Microsoft.Health.Dicom.Core/Messages/Operations/OperationStateResponse.cs deleted file mode 100644 index 683f21c3b8..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/Operations/OperationStateResponse.cs +++ /dev/null @@ -1,29 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using Microsoft.Health.Dicom.Core.Models.Operations; -using Microsoft.Health.Operations; - -namespace Microsoft.Health.Dicom.Core.Messages.Operations; - -/// -/// Represents a response with the state of long-running DICOM operations. -/// -public class OperationStateResponse -{ - /// - /// Initializes a new instance of the class. - /// - /// The state of the long-running operation. - public OperationStateResponse(IOperationState operationState) - => OperationState = EnsureArg.IsNotNull(operationState); - - /// - /// Gets the state of the long-running operation. - /// - /// The detailed operation state. - public IOperationState OperationState { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/Partitioning/GetOrAddPartitionRequest.cs b/src/Microsoft.Health.Dicom.Core/Messages/Partitioning/GetOrAddPartitionRequest.cs deleted file mode 100644 index 2f80987667..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/Partitioning/GetOrAddPartitionRequest.cs +++ /dev/null @@ -1,21 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using MediatR; - -namespace Microsoft.Health.Dicom.Core.Messages.Partitioning; - -public class GetOrAddPartitionRequest : IRequest -{ - public GetOrAddPartitionRequest(string partitionName) - { - PartitionName = partitionName; - } - - /// - /// Data Partition name - /// - public string PartitionName { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/Partitioning/GetOrAddPartitionResponse.cs b/src/Microsoft.Health.Dicom.Core/Messages/Partitioning/GetOrAddPartitionResponse.cs deleted file mode 100644 index 45e3057cf4..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/Partitioning/GetOrAddPartitionResponse.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - - -using Microsoft.Health.Dicom.Core.Features.Partitioning; - -namespace Microsoft.Health.Dicom.Core.Messages.Partitioning; - -public class GetOrAddPartitionResponse -{ - public GetOrAddPartitionResponse(Partition partition) - { - Partition = partition; - } - - public Partition Partition { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/Partitioning/GetPartitionRequest.cs b/src/Microsoft.Health.Dicom.Core/Messages/Partitioning/GetPartitionRequest.cs deleted file mode 100644 index a68e2ce836..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/Partitioning/GetPartitionRequest.cs +++ /dev/null @@ -1,21 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using MediatR; - -namespace Microsoft.Health.Dicom.Core.Messages.Partitioning; - -public class GetPartitionRequest : IRequest -{ - public GetPartitionRequest(string partitionName) - { - PartitionName = partitionName; - } - - /// - /// Data Partition name - /// - public string PartitionName { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/Partitioning/GetPartitionResponse.cs b/src/Microsoft.Health.Dicom.Core/Messages/Partitioning/GetPartitionResponse.cs deleted file mode 100644 index 8b96209a30..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/Partitioning/GetPartitionResponse.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - - -using Microsoft.Health.Dicom.Core.Features.Partitioning; - -namespace Microsoft.Health.Dicom.Core.Messages.Partitioning; - -public class GetPartitionResponse -{ - public GetPartitionResponse(Partition partition) - { - Partition = partition; - } - - public Partition Partition { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/Partitioning/GetPartitionsRequest.cs b/src/Microsoft.Health.Dicom.Core/Messages/Partitioning/GetPartitionsRequest.cs deleted file mode 100644 index 775413da3c..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/Partitioning/GetPartitionsRequest.cs +++ /dev/null @@ -1,15 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using MediatR; - -namespace Microsoft.Health.Dicom.Core.Messages.Partitioning; - -public class GetPartitionsRequest : IRequest -{ - public GetPartitionsRequest() - { - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/Partitioning/GetPartitionsResponse.cs b/src/Microsoft.Health.Dicom.Core/Messages/Partitioning/GetPartitionsResponse.cs deleted file mode 100644 index 72c94c18eb..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/Partitioning/GetPartitionsResponse.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using EnsureThat; -using Microsoft.Health.Dicom.Core.Features.Partitioning; - -namespace Microsoft.Health.Dicom.Core.Messages.Partitioning; - -public class GetPartitionsResponse -{ - public GetPartitionsResponse(IReadOnlyCollection entries) - { - EnsureArg.IsNotNull(entries, nameof(entries)); - - Entries = entries; - } - - public IReadOnlyCollection Entries { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/Query/QueryResourceRequest.cs b/src/Microsoft.Health.Dicom.Core/Messages/Query/QueryResourceRequest.cs deleted file mode 100644 index 887ba3dc8e..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/Query/QueryResourceRequest.cs +++ /dev/null @@ -1,18 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using MediatR; -using Microsoft.Health.Dicom.Core.Features.Query; - -namespace Microsoft.Health.Dicom.Core.Messages.Query; - -public class QueryResourceRequest : IRequest -{ - public QueryResourceRequest(QueryParameters parameters) - => Parameters = EnsureArg.IsNotNull(parameters, nameof(parameters)); - - public QueryParameters Parameters { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/Query/QueryResourceResponse.cs b/src/Microsoft.Health.Dicom.Core/Messages/Query/QueryResourceResponse.cs deleted file mode 100644 index a40c42ed8c..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/Query/QueryResourceResponse.cs +++ /dev/null @@ -1,23 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using EnsureThat; -using FellowOakDicom; - -namespace Microsoft.Health.Dicom.Core.Messages.Query; - -public sealed class QueryResourceResponse -{ - public QueryResourceResponse(IEnumerable responseDataset, IReadOnlyCollection erroneousTags) - { - ResponseDataset = EnsureArg.IsNotNull(responseDataset, nameof(responseDataset)); - ErroneousTags = EnsureArg.IsNotNull(erroneousTags, nameof(erroneousTags)); - } - - public IEnumerable ResponseDataset { get; } - - public IReadOnlyCollection ErroneousTags { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/ResourceType.cs b/src/Microsoft.Health.Dicom.Core/Messages/ResourceType.cs deleted file mode 100644 index 177a78119d..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/ResourceType.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Messages; - -public enum ResourceType -{ - Study, - Series, - Instance, - Frames, -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/Retrieve/AcceptHeader.cs b/src/Microsoft.Health.Dicom.Core/Messages/Retrieve/AcceptHeader.cs deleted file mode 100644 index a8d99022e9..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/Retrieve/AcceptHeader.cs +++ /dev/null @@ -1,39 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Microsoft.Extensions.Primitives; -using Microsoft.Health.Dicom.Core.Features.Retrieve; - -namespace Microsoft.Health.Dicom.Core.Messages.Retrieve; - -public class AcceptHeader -{ - public const double DefaultQuality = 1.0; - public AcceptHeader(StringSegment mediaType, PayloadTypes payloadType, StringSegment transferSyntax = default, double? quality = DefaultQuality) - { - MediaType = mediaType; - PayloadType = payloadType; - TransferSyntax = transferSyntax; - Quality = quality; - } - - public StringSegment MediaType { get; } - - public PayloadTypes PayloadType { get; } - - public StringSegment TransferSyntax { get; } - - public double? Quality { get; } - - public bool IsSinglePart - { - get { return PayloadType == PayloadTypes.SinglePart; } - } - - public override string ToString() - { - return $"MediaType:'{MediaType}', PayloadType:'{PayloadType}', TransferSyntax:'{TransferSyntax}', Quality:'{Quality}'"; - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/Retrieve/RetrieveMetadataRequest.cs b/src/Microsoft.Health.Dicom.Core/Messages/Retrieve/RetrieveMetadataRequest.cs deleted file mode 100644 index 9afecd2ec5..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/Retrieve/RetrieveMetadataRequest.cs +++ /dev/null @@ -1,50 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using MediatR; - -namespace Microsoft.Health.Dicom.Core.Messages.Retrieve; - -public class RetrieveMetadataRequest : IRequest -{ - public RetrieveMetadataRequest(string studyInstanceUid, string ifNoneMatch, bool isOriginalVersionRequested = false) - { - StudyInstanceUid = studyInstanceUid; - ResourceType = ResourceType.Study; - IfNoneMatch = ifNoneMatch; - IsOriginalVersionRequested = isOriginalVersionRequested; - } - - public RetrieveMetadataRequest(string studyInstanceUid, string seriesInstanceUid, string ifNoneMatch, bool isOriginalVersionRequested = false) - { - StudyInstanceUid = studyInstanceUid; - SeriesInstanceUid = seriesInstanceUid; - ResourceType = ResourceType.Series; - IfNoneMatch = ifNoneMatch; - IsOriginalVersionRequested = isOriginalVersionRequested; - } - - public RetrieveMetadataRequest(string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid, string ifNoneMatch, bool isOriginalVersionRequested = false) - { - StudyInstanceUid = studyInstanceUid; - SeriesInstanceUid = seriesInstanceUid; - SopInstanceUid = sopInstanceUid; - ResourceType = ResourceType.Instance; - IfNoneMatch = ifNoneMatch; - IsOriginalVersionRequested = isOriginalVersionRequested; - } - - public ResourceType ResourceType { get; } - - public string StudyInstanceUid { get; } - - public string SeriesInstanceUid { get; } - - public string SopInstanceUid { get; } - - public string IfNoneMatch { get; } - - public bool IsOriginalVersionRequested { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/Retrieve/RetrieveMetadataResponse.cs b/src/Microsoft.Health.Dicom.Core/Messages/Retrieve/RetrieveMetadataResponse.cs deleted file mode 100644 index c46257b092..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/Retrieve/RetrieveMetadataResponse.cs +++ /dev/null @@ -1,27 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using EnsureThat; -using FellowOakDicom; - -namespace Microsoft.Health.Dicom.Core.Messages.Retrieve; - -public class RetrieveMetadataResponse -{ - public RetrieveMetadataResponse(IAsyncEnumerable responseMetadata, bool isCacheValid = false, string eTag = null) - { - EnsureArg.IsNotNull(responseMetadata, nameof(responseMetadata)); - ResponseMetadata = responseMetadata; - IsCacheValid = isCacheValid; - ETag = eTag; - } - - public IAsyncEnumerable ResponseMetadata { get; } - - public bool IsCacheValid { get; } - - public string ETag { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/Retrieve/RetrieveRenderedRequest.cs b/src/Microsoft.Health.Dicom.Core/Messages/Retrieve/RetrieveRenderedRequest.cs deleted file mode 100644 index 5bed3330af..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/Retrieve/RetrieveRenderedRequest.cs +++ /dev/null @@ -1,41 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using MediatR; - -namespace Microsoft.Health.Dicom.Core.Messages.Retrieve; -public class RetrieveRenderedRequest : IRequest -{ - - public RetrieveRenderedRequest(string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid, ResourceType resourceType, int frameNumber, int quality, IReadOnlyCollection acceptHeaders) - { - StudyInstanceUid = studyInstanceUid; - SeriesInstanceUid = seriesInstanceUid; - SopInstanceUid = sopInstanceUid; - ResourceType = resourceType; - - // Per DICOMWeb spec (http://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_9.5.1.2.1) - // frame number in the URI is 1-based, unlike fo-dicom representation where it's 0-based. - FrameNumber = frameNumber - 1; - - Quality = quality; - AcceptHeaders = acceptHeaders; - } - - public ResourceType ResourceType { get; } - - public IReadOnlyCollection AcceptHeaders { get; } - - public string StudyInstanceUid { get; } - - public string SeriesInstanceUid { get; } - - public string SopInstanceUid { get; } - - public int FrameNumber { get; } - - public int Quality { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/Retrieve/RetrieveRenderedResponse.cs b/src/Microsoft.Health.Dicom.Core/Messages/Retrieve/RetrieveRenderedResponse.cs deleted file mode 100644 index 97eecf5e53..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/Retrieve/RetrieveRenderedResponse.cs +++ /dev/null @@ -1,25 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.IO; -using EnsureThat; - -namespace Microsoft.Health.Dicom.Core.Messages.Retrieve; -public class RetrieveRenderedResponse -{ - public RetrieveRenderedResponse(Stream responseStream, long responseLength, string contentType) - { - ResponseStream = EnsureArg.IsNotNull(responseStream, nameof(responseStream)); - ContentType = EnsureArg.IsNotEmptyOrWhiteSpace(contentType, nameof(contentType)); - - ResponseLength = responseLength; - } - - public Stream ResponseStream { get; } - - public long ResponseLength { get; } - - public string ContentType { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/Retrieve/RetrieveRequestValidator.cs b/src/Microsoft.Health.Dicom.Core/Messages/Retrieve/RetrieveRequestValidator.cs deleted file mode 100644 index 68a5690d23..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/Retrieve/RetrieveRequestValidator.cs +++ /dev/null @@ -1,72 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Linq; -using EnsureThat; -using FellowOakDicom; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Validation; - -namespace Microsoft.Health.Dicom.Core.Messages.Retrieve; - -public static class RetrieveRequestValidator -{ - private const string UnknownDicomTransferSyntaxName = "Unknown"; - private const string StudyInstanceUid = "StudyInstanceUid"; - private const string SeriesInstanceUid = "SeriesInstanceUid"; - private const string SopInstanceUid = "SopInstanceUid"; - - public static void ValidateInstanceIdentifiers(ResourceType resourceType, string studyInstanceUid, string seriesInstanceUid = null, string sopInstanceUid = null) - { - EnsureArg.IsNotNullOrWhiteSpace(studyInstanceUid, nameof(studyInstanceUid)); - - ValidateInstanceIdentifiersAreValid(resourceType, studyInstanceUid, seriesInstanceUid, sopInstanceUid); - } - - private static void ValidateInstanceIdentifiersAreValid(ResourceType resourceType, string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid) - { - UidValidation.Validate(studyInstanceUid, nameof(StudyInstanceUid)); - - switch (resourceType) - { - case ResourceType.Series: - UidValidation.Validate(seriesInstanceUid, nameof(SeriesInstanceUid)); - break; - case ResourceType.Instance: - case ResourceType.Frames: - UidValidation.Validate(seriesInstanceUid, nameof(SeriesInstanceUid)); - UidValidation.Validate(sopInstanceUid, nameof(SopInstanceUid)); - break; - } - } - - public static void ValidateFrames(IReadOnlyCollection frames) - { - if (frames == null || frames.Count == 0 || frames.Any(x => x < 0)) - throw new BadRequestException(DicomCoreResource.InvalidFramesValue); - - } - - public static void ValidateTransferSyntax(string requestedTransferSyntax, bool originalTransferSyntaxRequested = false) - { - if (!originalTransferSyntaxRequested && !string.IsNullOrEmpty(requestedTransferSyntax)) - { - try - { - DicomTransferSyntax transferSyntax = DicomTransferSyntax.Parse(requestedTransferSyntax); - - if (transferSyntax?.UID == null || transferSyntax.UID.Name == UnknownDicomTransferSyntaxName) - { - throw new BadRequestException(DicomCoreResource.InvalidTransferSyntaxValue); - } - } - catch (DicomDataException) - { - throw new BadRequestException(DicomCoreResource.InvalidTransferSyntaxValue); - } - } - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/Retrieve/RetrieveResourceInstance.cs b/src/Microsoft.Health.Dicom.Core/Messages/Retrieve/RetrieveResourceInstance.cs deleted file mode 100644 index 101dbf06bd..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/Retrieve/RetrieveResourceInstance.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.IO; - -namespace Microsoft.Health.Dicom.Core.Messages.Retrieve; - -public class RetrieveResourceInstance -{ - public RetrieveResourceInstance(Stream stream, string transferSyntaxUid = null, long streamLength = 0) - { - Stream = stream; - TransferSyntaxUid = transferSyntaxUid; - StreamLength = streamLength; - } - - public Stream Stream { get; } - public string TransferSyntaxUid { get; } - public long StreamLength { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/Retrieve/RetrieveResourceRequest.cs b/src/Microsoft.Health.Dicom.Core/Messages/Retrieve/RetrieveResourceRequest.cs deleted file mode 100644 index 7f600eb42f..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/Retrieve/RetrieveResourceRequest.cs +++ /dev/null @@ -1,74 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Linq; -using MediatR; - -namespace Microsoft.Health.Dicom.Core.Messages.Retrieve; - -public class RetrieveResourceRequest : IRequest -{ - public RetrieveResourceRequest(string studyInstanceUid, IReadOnlyCollection acceptHeaders, bool isOriginalVersionRequested = false) - : this(ResourceType.Study, acceptHeaders, isOriginalVersionRequested) - { - StudyInstanceUid = studyInstanceUid; - } - - public RetrieveResourceRequest(string studyInstanceUid, string seriesInstanceUid, IReadOnlyCollection acceptHeaders, bool isOriginalVersionRequested = false) - : this(ResourceType.Series, acceptHeaders, isOriginalVersionRequested) - { - StudyInstanceUid = studyInstanceUid; - SeriesInstanceUid = seriesInstanceUid; - } - - public RetrieveResourceRequest( - string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid, IReadOnlyCollection acceptHeaders, bool isOriginalVersionRequested = false) - : this(ResourceType.Instance, acceptHeaders, isOriginalVersionRequested) - { - StudyInstanceUid = studyInstanceUid; - SeriesInstanceUid = seriesInstanceUid; - SopInstanceUid = sopInstanceUid; - } - - public RetrieveResourceRequest( - string studyInstanceUid, - string seriesInstanceUid, - string sopInstanceUid, - IReadOnlyCollection frames, - IReadOnlyCollection acceptHeaders, - bool isOriginalVersionRequested = false) - : this(ResourceType.Frames, acceptHeaders, isOriginalVersionRequested) - { - StudyInstanceUid = studyInstanceUid; - SeriesInstanceUid = seriesInstanceUid; - SopInstanceUid = sopInstanceUid; - - // Per DICOMWeb spec (http://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_9.5.1.2.1) - // frame number in the URI is 1-based, unlike fo-dicom representation where it's 0-based. - Frames = frames?.Select(x => x - 1).ToList(); - } - - private RetrieveResourceRequest(ResourceType resourceType, IReadOnlyCollection acceptHeaders, bool isOriginalVersionRequested) - { - ResourceType = resourceType; - AcceptHeaders = acceptHeaders; - IsOriginalVersionRequested = isOriginalVersionRequested; - } - - public ResourceType ResourceType { get; } - - public string StudyInstanceUid { get; } - - public string SeriesInstanceUid { get; } - - public string SopInstanceUid { get; } - - public IReadOnlyCollection Frames { get; } - - public IReadOnlyCollection AcceptHeaders { get; } - - public bool IsOriginalVersionRequested { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/Retrieve/RetrieveResourceResponse.cs b/src/Microsoft.Health.Dicom.Core/Messages/Retrieve/RetrieveResourceResponse.cs deleted file mode 100644 index 823ed85a24..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/Retrieve/RetrieveResourceResponse.cs +++ /dev/null @@ -1,34 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Threading; -using EnsureThat; - -namespace Microsoft.Health.Dicom.Core.Messages.Retrieve; - -public class RetrieveResourceResponse -{ - public RetrieveResourceResponse(IAsyncEnumerable responseStreams, string contentType, bool isSinglePart = false) - { - ResponseInstances = EnsureArg.IsNotNull(responseStreams, nameof(responseStreams)); - ContentType = EnsureArg.IsNotEmptyOrWhiteSpace(contentType, nameof(contentType)); - IsSinglePart = isSinglePart; - } - - /// - /// Collection of instance streams and properties used in response - /// - public IAsyncEnumerable ResponseInstances { get; } - - public string ContentType { get; } - - public bool IsSinglePart { get; } - - public IAsyncEnumerator GetResponseInstancesEnumerator(CancellationToken cancellationToken) - { - return ResponseInstances.GetAsyncEnumerator(cancellationToken); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/Store/StoreRequest.cs b/src/Microsoft.Health.Dicom.Core/Messages/Store/StoreRequest.cs deleted file mode 100644 index 53918d5d6c..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/Store/StoreRequest.cs +++ /dev/null @@ -1,28 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.IO; -using MediatR; - -namespace Microsoft.Health.Dicom.Core.Messages.Store; - -public class StoreRequest : IRequest -{ - public StoreRequest( - Stream requestBody, - string requestContentType, - string studyInstanceUid = null) - { - StudyInstanceUid = studyInstanceUid; - RequestBody = requestBody; - RequestContentType = requestContentType; - } - - public string StudyInstanceUid { get; } - - public Stream RequestBody { get; } - - public string RequestContentType { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/Store/StoreResponse.cs b/src/Microsoft.Health.Dicom.Core/Messages/Store/StoreResponse.cs deleted file mode 100644 index 03b708a31f..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/Store/StoreResponse.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using FellowOakDicom; - -namespace Microsoft.Health.Dicom.Core.Messages.Store; - -public sealed class StoreResponse -{ - public StoreResponse(StoreResponseStatus status, DicomDataset responseDataset, string warning) - { - Status = status; - Dataset = responseDataset; - Warning = warning; - } - - public StoreResponseStatus Status { get; } - - public DicomDataset Dataset { get; } - - public string Warning { get; set; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/Store/StoreResponseStatus.cs b/src/Microsoft.Health.Dicom.Core/Messages/Store/StoreResponseStatus.cs deleted file mode 100644 index ed5896685e..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/Store/StoreResponseStatus.cs +++ /dev/null @@ -1,38 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Messages.Store; - -/// -/// Represents the store transaction response status. -/// -public enum StoreResponseStatus -{ - /// - /// There is no DICOM instance to store. - /// - None, - - /// - /// The origin server successfully stored all Instances. - /// - Success, - - /// - /// The origin server stored some of the Instances and failures exist for others. - /// Or origin server stored has warnings for Instances stored. - /// Additional information regarding this error may be found in the response message body. - /// - PartialSuccess, - - /// - /// The origin server was unable to store any instances due to bad syntax. - /// The request was formed correctly but the origin server was unable to store any instances due to a conflict - /// in the request (e.g., unsupported SOP Class or Study Instance UID mismatch). - /// This may also be used to indicate that the origin server was unable to store any instances for a mixture of reasons. - /// Additional information regarding the instance errors may be found in the payload. - /// - Failure, -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/Update/UpdateInstanceRequest.cs b/src/Microsoft.Health.Dicom.Core/Messages/Update/UpdateInstanceRequest.cs deleted file mode 100644 index 0c523ca492..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/Update/UpdateInstanceRequest.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using MediatR; -using Microsoft.Health.Dicom.Core.Models.Update; - -namespace Microsoft.Health.Dicom.Core.Messages.Update; - -public class UpdateInstanceRequest : IRequest -{ - public UpdateInstanceRequest(UpdateSpecification updateSpec) - { - UpdateSpec = updateSpec; - } - - public UpdateSpecification UpdateSpec { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/Update/UpdateInstanceResponse.cs b/src/Microsoft.Health.Dicom.Core/Messages/Update/UpdateInstanceResponse.cs deleted file mode 100644 index 67589de71b..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/Update/UpdateInstanceResponse.cs +++ /dev/null @@ -1,27 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using FellowOakDicom; -using Microsoft.Health.Operations; - -namespace Microsoft.Health.Dicom.Core.Messages.Update; - -public class UpdateInstanceResponse -{ - public UpdateInstanceResponse(OperationReference operationReference) - { - Operation = EnsureArg.IsNotNull(operationReference); - } - - public UpdateInstanceResponse(DicomDataset dataset) - { - FailedDataset = EnsureArg.IsNotNull(dataset); - } - - public OperationReference Operation { get; } - - public DicomDataset FailedDataset { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/WorkItem/AddWorkitemRequest.cs b/src/Microsoft.Health.Dicom.Core/Messages/WorkItem/AddWorkitemRequest.cs deleted file mode 100644 index 62276431f7..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/WorkItem/AddWorkitemRequest.cs +++ /dev/null @@ -1,28 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using FellowOakDicom; -using MediatR; - -namespace Microsoft.Health.Dicom.Core.Messages.Workitem; - -public class AddWorkitemRequest : IRequest -{ - public AddWorkitemRequest( - DicomDataset dicomDataset, - string requestContentType, - string workItemInstanceUid) - { - WorkitemInstanceUid = workItemInstanceUid; - DicomDataset = dicomDataset; - RequestContentType = requestContentType; - } - - public string WorkitemInstanceUid { get; } - - public DicomDataset DicomDataset { get; } - - public string RequestContentType { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/WorkItem/AddWorkitemResponse.cs b/src/Microsoft.Health.Dicom.Core/Messages/WorkItem/AddWorkitemResponse.cs deleted file mode 100644 index e980b95ff6..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/WorkItem/AddWorkitemResponse.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; - -namespace Microsoft.Health.Dicom.Core.Messages.Workitem; - -public sealed class AddWorkitemResponse -{ - public AddWorkitemResponse(WorkitemResponseStatus status, Uri uri, string message = null) - { - Status = status; - Uri = uri; - Message = message; - } - - public WorkitemResponseStatus Status { get; } - - public Uri Uri { get; } - - public string Message { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/WorkItem/CancelWorkitemRequest.cs b/src/Microsoft.Health.Dicom.Core/Messages/WorkItem/CancelWorkitemRequest.cs deleted file mode 100644 index 01b9799406..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/WorkItem/CancelWorkitemRequest.cs +++ /dev/null @@ -1,28 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using FellowOakDicom; -using MediatR; - -namespace Microsoft.Health.Dicom.Core.Messages.Workitem; - -public class CancelWorkitemRequest : IRequest -{ - public CancelWorkitemRequest( - DicomDataset dicomDataset, - string requestContentType, - string workItemInstanceUid) - { - WorkitemInstanceUid = workItemInstanceUid; - DicomDataset = dicomDataset; - RequestContentType = requestContentType; - } - - public string WorkitemInstanceUid { get; } - - public DicomDataset DicomDataset { get; } - - public string RequestContentType { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/WorkItem/CancelWorkitemResponse.cs b/src/Microsoft.Health.Dicom.Core/Messages/WorkItem/CancelWorkitemResponse.cs deleted file mode 100644 index a328b05899..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/WorkItem/CancelWorkitemResponse.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Messages.Workitem; - -public sealed class CancelWorkitemResponse -{ - public CancelWorkitemResponse(WorkitemResponseStatus status, string message = null) - { - Status = status; - Message = message; - } - - public WorkitemResponseStatus Status { get; } - - public string Message { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/WorkItem/ChangeWorkitemStateRequest.cs b/src/Microsoft.Health.Dicom.Core/Messages/WorkItem/ChangeWorkitemStateRequest.cs deleted file mode 100644 index d326a36298..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/WorkItem/ChangeWorkitemStateRequest.cs +++ /dev/null @@ -1,23 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using FellowOakDicom; -using MediatR; - -namespace Microsoft.Health.Dicom.Core.Messages.Workitem; - -public sealed class ChangeWorkitemStateRequest : IRequest -{ - public ChangeWorkitemStateRequest(DicomDataset dicomDataset, string requestContentType, string workitemInstanceUid) - { - DicomDataset = dicomDataset; - RequestContentType = requestContentType; - WorkitemInstanceUid = workitemInstanceUid; - } - - public DicomDataset DicomDataset { get; } - public string RequestContentType { get; } - public string WorkitemInstanceUid { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/WorkItem/ChangeWorkitemStateResponse.cs b/src/Microsoft.Health.Dicom.Core/Messages/WorkItem/ChangeWorkitemStateResponse.cs deleted file mode 100644 index 7f36983bb8..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/WorkItem/ChangeWorkitemStateResponse.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Messages.Workitem; - -public sealed class ChangeWorkitemStateResponse -{ - public ChangeWorkitemStateResponse(WorkitemResponseStatus status, string message = null) - { - Status = status; - Message = message; - } - - public WorkitemResponseStatus Status { get; } - - public string Message { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/WorkItem/QueryWorkitemResourceRequest.cs b/src/Microsoft.Health.Dicom.Core/Messages/WorkItem/QueryWorkitemResourceRequest.cs deleted file mode 100644 index a733f7380e..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/WorkItem/QueryWorkitemResourceRequest.cs +++ /dev/null @@ -1,18 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using MediatR; -using Microsoft.Health.Dicom.Core.Features.Query; - -namespace Microsoft.Health.Dicom.Core.Messages.Workitem; - -public class QueryWorkitemResourceRequest : IRequest -{ - public QueryWorkitemResourceRequest(BaseQueryParameters parameters) - => Parameters = EnsureArg.IsNotNull(parameters, nameof(parameters)); - - public BaseQueryParameters Parameters { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/WorkItem/QueryWorkitemResourceResponse.cs b/src/Microsoft.Health.Dicom.Core/Messages/WorkItem/QueryWorkitemResourceResponse.cs deleted file mode 100644 index efd535333f..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/WorkItem/QueryWorkitemResourceResponse.cs +++ /dev/null @@ -1,23 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using EnsureThat; -using FellowOakDicom; - -namespace Microsoft.Health.Dicom.Core.Messages.Workitem; - -public sealed class QueryWorkitemResourceResponse -{ - public QueryWorkitemResourceResponse(IReadOnlyList responseDataset, WorkitemResponseStatus status) - { - ResponseDatasets = EnsureArg.IsNotNull(responseDataset, nameof(responseDataset)); - Status = status; - } - - public WorkitemResponseStatus Status { get; } - - public IReadOnlyList ResponseDatasets { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/WorkItem/RetrieveWorkitemRequest.cs b/src/Microsoft.Health.Dicom.Core/Messages/WorkItem/RetrieveWorkitemRequest.cs deleted file mode 100644 index 5ecb57f87f..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/WorkItem/RetrieveWorkitemRequest.cs +++ /dev/null @@ -1,18 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using MediatR; - -namespace Microsoft.Health.Dicom.Core.Messages.Workitem; - -public sealed class RetrieveWorkitemRequest : IRequest -{ - public RetrieveWorkitemRequest(string workitemInstanceUid) - { - WorkitemInstanceUid = workitemInstanceUid; - } - - public string WorkitemInstanceUid { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/WorkItem/RetrieveWorkitemResponse.cs b/src/Microsoft.Health.Dicom.Core/Messages/WorkItem/RetrieveWorkitemResponse.cs deleted file mode 100644 index 4a6192b174..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/WorkItem/RetrieveWorkitemResponse.cs +++ /dev/null @@ -1,25 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using FellowOakDicom; - -namespace Microsoft.Health.Dicom.Core.Messages.Workitem; - -public sealed class RetrieveWorkitemResponse -{ - public RetrieveWorkitemResponse(WorkitemResponseStatus status, DicomDataset responseDataset, string message = null) - { - Dataset = EnsureArg.IsNotNull(responseDataset, nameof(responseDataset)); - Status = status; - Message = message; - } - - public DicomDataset Dataset { get; } - - public WorkitemResponseStatus Status { get; } - - public string Message { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/WorkItem/UpdateWorkitemRequest.cs b/src/Microsoft.Health.Dicom.Core/Messages/WorkItem/UpdateWorkitemRequest.cs deleted file mode 100644 index 67ae24ff0a..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/WorkItem/UpdateWorkitemRequest.cs +++ /dev/null @@ -1,28 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using FellowOakDicom; -using MediatR; - -namespace Microsoft.Health.Dicom.Core.Messages.Workitem; - -public sealed class UpdateWorkitemRequest : IRequest -{ - public UpdateWorkitemRequest(DicomDataset dicomDataset, string requestContentType, string workitemInstanceUid, string transactionUid) - { - DicomDataset = dicomDataset; - RequestContentType = requestContentType; - WorkitemInstanceUid = workitemInstanceUid; - TransactionUid = transactionUid; - } - - public string WorkitemInstanceUid { get; } - - public string TransactionUid { get; } - - public DicomDataset DicomDataset { get; } - - public string RequestContentType { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/WorkItem/UpdateWorkitemResponse.cs b/src/Microsoft.Health.Dicom.Core/Messages/WorkItem/UpdateWorkitemResponse.cs deleted file mode 100644 index 6cb1012c51..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/WorkItem/UpdateWorkitemResponse.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; - -namespace Microsoft.Health.Dicom.Core.Messages.Workitem; - -public sealed class UpdateWorkitemResponse -{ - public UpdateWorkitemResponse(WorkitemResponseStatus status, Uri uri, string message = null) - { - Status = status; - Uri = uri; - Message = message; - } - - public WorkitemResponseStatus Status { get; } - - public Uri Uri { get; } - - public string Message { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Messages/WorkItem/WorkitemResponseStatus.cs b/src/Microsoft.Health.Dicom.Core/Messages/WorkItem/WorkitemResponseStatus.cs deleted file mode 100644 index 863442e9b5..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Messages/WorkItem/WorkitemResponseStatus.cs +++ /dev/null @@ -1,47 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Messages.Workitem; - -/// -/// Represents the add work-item response status. -/// -public enum WorkitemResponseStatus -{ - /// - /// There is no DICOM instance to add. - /// - None, - - /// - /// The DICOM work-item instance is not found - /// - NotFound, - - /// - /// All DICOM work-item instance(s) have been add successfully. - /// - Success, - - /// - /// All DICOM work-item instance(s) have failed to add. - /// - Failure, - - /// - /// Workitem instance already exist. - /// - Conflict, - - /// - /// All matching workitem instance(s) found - /// - NoContent, - - /// - /// Only partial workitem instance(s) found - /// - PartialContent, -} diff --git a/src/Microsoft.Health.Dicom.Core/Microsoft.Health.Dicom.Core.csproj b/src/Microsoft.Health.Dicom.Core/Microsoft.Health.Dicom.Core.csproj deleted file mode 100644 index 94d8cd6521..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Microsoft.Health.Dicom.Core.csproj +++ /dev/null @@ -1,75 +0,0 @@ - - - - Common primitives and utilities used by Microsoft's DICOMweb APIs. - $(LibraryFrameworks) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Features\Workitem\WorkitemDatasetValidator.cs - - - - - - Features\Workitem\WorkitemService.cs - - - - - - - - - - True - True - DicomCoreResource.resx - - - - - - ResXFileCodeGenerator - DicomCoreResource.Designer.cs - - - - diff --git a/src/Microsoft.Health.Dicom.Core/Models/Delete/DeleteMetrics.cs b/src/Microsoft.Health.Dicom.Core/Models/Delete/DeleteMetrics.cs deleted file mode 100644 index 3b869be0c6..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Models/Delete/DeleteMetrics.cs +++ /dev/null @@ -1,33 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; - -namespace Microsoft.Health.Dicom.Core.Models.Delete; - -public readonly struct DeleteMetrics : IEquatable -{ - public DateTimeOffset OldestDeletion { get; init; } - - public int TotalExhaustedRetries { get; init; } - - public bool Equals(DeleteMetrics other) - { - return OldestDeletion == other.OldestDeletion - && TotalExhaustedRetries == other.TotalExhaustedRetries; - } - - public override bool Equals(object obj) - => obj is DeleteMetrics other && Equals(other); - - public override int GetHashCode() - => HashCode.Combine(OldestDeletion, TotalExhaustedRetries); - - public static bool operator ==(DeleteMetrics left, DeleteMetrics right) - => Equals(left, right); - - public static bool operator !=(DeleteMetrics left, DeleteMetrics right) - => !Equals(left, right); -} diff --git a/src/Microsoft.Health.Dicom.Core/Models/DicomIdentifier.cs b/src/Microsoft.Health.Dicom.Core/Models/DicomIdentifier.cs deleted file mode 100644 index e922319486..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Models/DicomIdentifier.cs +++ /dev/null @@ -1,289 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Globalization; -using EnsureThat; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Features.Validation; -using Microsoft.Health.Dicom.Core.Messages; - -namespace Microsoft.Health.Dicom.Core.Models.Common; - -/// -/// Represents an identifier for a DICOM study, series, or SOP instance. -/// -public readonly struct DicomIdentifier : IEquatable -{ - /// - /// Gets a value indicating whether the identifier is the default empty identifier. - /// - /// - /// This value is considered to represent a blank study. - /// - /// if the value is blank; otherwise, . - public bool IsEmpty => StudyInstanceUid == null; - - /// - /// Gets the corresponding for this identifier. - /// - /// The value. - public ResourceType Type - { - get - { - // TODO: Frame? - if (SopInstanceUid != null) - return ResourceType.Instance; - else if (SeriesInstanceUid != null) - return ResourceType.Series; - else - return ResourceType.Study; - } - } - - /// - /// Gets the unique identifier for the study. - /// - /// The value of the study instance UID attribute (0020,000D). - public string StudyInstanceUid { get; } - - /// - /// Gets the unique identifier for the series. - /// - /// - /// May be if the value of the property is . - /// - /// The value of the series instance UID attribute (0020,000E). - public string SeriesInstanceUid { get; } - - /// - /// Gets the unique identifier for the SOP instance. - /// - /// - /// May be if the value of the property is - /// or . - /// - /// The value of the SOP instance UID attribute (0008,0018). - public string SopInstanceUid { get; } - - /// - /// Initializes a new instance of the structure based on the given identifiers. - /// - /// The unique identifier for the study. - /// The optional unique identifier for the series. - /// The optional unique identifier for the SOP instance. - /// - /// is or white space - /// -or- - /// - /// is or white space, but - /// has a value. - /// - /// - public DicomIdentifier(string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid) - { - StudyInstanceUid = EnsureArg.IsNotNullOrWhiteSpace(studyInstanceUid, nameof(studyInstanceUid)); - if (string.IsNullOrWhiteSpace(seriesInstanceUid) && !string.IsNullOrWhiteSpace(sopInstanceUid)) - throw new ArgumentNullException(nameof(seriesInstanceUid)); - - SeriesInstanceUid = seriesInstanceUid; - SopInstanceUid = sopInstanceUid; - } - - /// - /// Returns a value indicating whether this instance is equal to a specified object. - /// - /// The object to compare to this instance. - /// - /// if is an instance of and - /// equals the value of this instance; otherwise, . - /// - public override bool Equals(object obj) - => obj is DicomIdentifier other && Equals(other); - - /// - /// Returns a value indicating whether the value of this instance is equal to the value of the - /// specified instance. - /// - /// The object to compare to this instance. - /// - /// if the parameter equals the - /// value of this instance; otherwise, . - /// - public bool Equals(DicomIdentifier other) - => string.Equals(StudyInstanceUid, other.StudyInstanceUid, StringComparison.Ordinal) - && string.Equals(SeriesInstanceUid, other.SeriesInstanceUid, StringComparison.Ordinal) - && string.Equals(SopInstanceUid, other.SopInstanceUid, StringComparison.Ordinal); - - /// - /// Returns the hash code for this instance. - /// - /// A 32-bit signed integer hash code. - public override int GetHashCode() - => HashCode.Combine(StudyInstanceUid, SeriesInstanceUid, SopInstanceUid); - - /// - /// Converts the value of the current structure to its equivalent string representation. - /// - /// A string representation of the value of the current structure. - public override string ToString() - { - if (SopInstanceUid != null) - return StudyInstanceUid + '/' + SeriesInstanceUid + '/' + SopInstanceUid; - else if (SeriesInstanceUid != null) - return StudyInstanceUid + '/' + SeriesInstanceUid; - else - return StudyInstanceUid; - } - - /// - /// Creates a new structure for the given study. - /// - /// The unique identifier for the study. - /// A structure for the given study. - /// is . - public static DicomIdentifier ForStudy(string uid) - => new DicomIdentifier(uid, null, null); - - /// - /// Creates a new structure for the given series. - /// - /// The unique identifier for the study. - /// The unique identifier for the series. - /// A structure for the given series. - /// - /// or is . - /// - public static DicomIdentifier ForSeries(string studyInstanceUid, string seriesInstanceUid) - => new DicomIdentifier(studyInstanceUid, EnsureArg.IsNotNullOrWhiteSpace(seriesInstanceUid, nameof(seriesInstanceUid)), null); - - /// - /// Creates a new structure for the given SOP instance. - /// - /// The unique identifier for the study. - /// The unique identifier for the series. - /// The unique identifier for the SOP instance. - /// A structure for the given SOP instance. - /// - /// , , - /// is . - /// - public static DicomIdentifier ForInstance(string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid) - => new DicomIdentifier( - studyInstanceUid, - EnsureArg.IsNotNullOrWhiteSpace(seriesInstanceUid, nameof(seriesInstanceUid)), - EnsureArg.IsNotNullOrWhiteSpace(sopInstanceUid, nameof(sopInstanceUid))); - - /// - /// Creates a new structure equivalent to the given . - /// - /// A versioned instance identifier. - /// A structure for the given SOP instance. - /// is . - public static DicomIdentifier ForInstance(VersionedInstanceIdentifier identifier) - => new DicomIdentifier( - EnsureArg.IsNotNull(identifier, nameof(identifier)).StudyInstanceUid, - identifier.SeriesInstanceUid, - identifier.SopInstanceUid); - - /// - /// Converts the string representation of a to its equivalent structure representation. - /// - /// A string that contains a to convert. - /// - /// An object that is equivalent to the contained in . - /// - /// is . - /// - /// does not contain a valid string representation of a . - /// - public static DicomIdentifier Parse(string value) - => TryParse(EnsureArg.IsNotNull(value, nameof(value)), out DicomIdentifier result) - ? result - : throw new FormatException(string.Format(CultureInfo.CurrentCulture, DicomCoreResource.InvalidDicomIdentifier, value)); - - /// - /// Converts the string representation of a to its equivalent structure representation - /// and returns a value that indicates whether the conversion succeeded. - /// - /// A string that contains a to convert. - /// - /// When this method returns, contains the value equivalent identifier - /// contained in , if the conversion succeeded, or the default value if the conversion - /// failed. The conversion fails if the parameter is , - /// is an empty string (""), or does not contain a valid string representation of an identifier. - /// - /// - /// if the parameter was converted successfully; - /// otherwise, . - /// - public static bool TryParse(string value, out DicomIdentifier result) - { - string[] parts = EnsureArg.IsNotNull(value, nameof(value)).Split('/', StringSplitOptions.TrimEntries); - if (parts.Length == 0 || parts.Length > 3) - { - result = default; - return false; - } - - string studyInstanceUid, seriesInstanceUid = null, sopInstanceUid = null; - - // Parse and Validate - studyInstanceUid = parts[0]; - if (!UidValidation.IsValid(studyInstanceUid)) - { - result = default; - return false; - } - - if (parts.Length > 1) - { - seriesInstanceUid = parts[1]; - if (!UidValidation.IsValid(seriesInstanceUid)) - { - result = default; - return false; - } - - if (parts.Length == 3) - { - sopInstanceUid = parts[2]; - if (!UidValidation.IsValid(sopInstanceUid)) - { - result = default; - return false; - } - } - } - - result = new DicomIdentifier(studyInstanceUid, seriesInstanceUid, sopInstanceUid); - return true; - } - - /// - /// Determines whether two specified instances of are equal. - /// - /// The first object to compare. - /// The second object to compare. - /// - /// if and - /// represent the same ; otherwise, . - /// - public static bool operator ==(DicomIdentifier left, DicomIdentifier right) - => left.Equals(right); - - /// - /// Determines whether two specified instances of are not equal. - /// - /// The first object to compare. - /// The second object to compare. - /// - /// if and - /// do not represent the same ; otherwise, . - /// - public static bool operator !=(DicomIdentifier left, DicomIdentifier right) - => !left.Equals(right); -} diff --git a/src/Microsoft.Health.Dicom.Core/Models/Export/AzureBlobExportOptions.cs b/src/Microsoft.Health.Dicom.Core/Models/Export/AzureBlobExportOptions.cs deleted file mode 100644 index 1eaecf6b65..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Models/Export/AzureBlobExportOptions.cs +++ /dev/null @@ -1,28 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using Newtonsoft.Json; - -namespace Microsoft.Health.Dicom.Core.Models.Export; - -internal sealed class AzureBlobExportOptions -{ - public Uri BlobContainerUri { get; set; } - - public string ConnectionString { get; set; } - - public string BlobContainerName { get; set; } - - public bool UseManagedIdentity { get; set; } - - [JsonProperty] // Newtonsoft is only used internally while this property would be ignored by System.Text.Json - internal SecretKey Secret { get; set; } - - // TODO: Make public upon request. Perhaps a boolean flag instead? - internal const string DicomFilePattern = "%Operation%/results/%Study%/%Series%/%SopInstance%.dcm"; - - internal const string ErrorLogPattern = "%Operation%/errors.log"; -} diff --git a/src/Microsoft.Health.Dicom.Core/Models/Export/ExportDataOptions.cs b/src/Microsoft.Health.Dicom.Core/Models/Export/ExportDataOptions.cs deleted file mode 100644 index 6bec9e3b98..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Models/Export/ExportDataOptions.cs +++ /dev/null @@ -1,39 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; - -namespace Microsoft.Health.Dicom.Core.Models.Export; - -/// -/// Represents options for either what data should be exported from the DICOM service, -/// or where that data should be copied. -/// -public sealed class ExportDataOptions -{ - /// - /// Creates a new instance of the class - /// with the given type. - /// - /// The type of options this new instance represents. - public ExportDataOptions(T type) - { - Type = type; - } - - internal ExportDataOptions(T type, object settings) - { - Type = type; - Settings = EnsureArg.IsNotNull(settings, nameof(settings)); - } - - /// - /// Gets the type of options this instance represents. - /// - /// A type denoting the kind of options. - public T Type { get; } - - internal object Settings { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Models/Export/ExportDestinationType.cs b/src/Microsoft.Health.Dicom.Core/Models/Export/ExportDestinationType.cs deleted file mode 100644 index 3c91fe8784..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Models/Export/ExportDestinationType.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Models.Export; - -/// -/// Specifies the kind of destination where data may be exported. -/// -public enum ExportDestinationType -{ - /// - /// Specifies an unknown destination. - /// - Unknown, - - /// - /// Specifies Azure Blob Storage. - /// - AzureBlob, -} diff --git a/src/Microsoft.Health.Dicom.Core/Models/Export/ExportErrorLogEntry.cs b/src/Microsoft.Health.Dicom.Core/Models/Export/ExportErrorLogEntry.cs deleted file mode 100644 index 72db46cf51..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Models/Export/ExportErrorLogEntry.cs +++ /dev/null @@ -1,33 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using Microsoft.Health.Dicom.Core.Models.Common; - -namespace Microsoft.Health.Dicom.Core.Models.Export; - -/// -/// Represents an entry in the export error log. -/// -public sealed class ExportErrorLogEntry -{ - /// - /// Gets or sets the timestamp that the error ocurred. - /// - /// The date and time of the error. - public DateTimeOffset Timestamp { get; set; } - - /// - /// Gets or sets the identifier for the study, series, or SOP instance that failed to export. - /// - /// The identifier for the failed DICOM file(s). - public DicomIdentifier Identifier { get; set; } - - /// - /// Gets or sets the error message detailing why the export failed. - /// - /// The error message. - public string Error { get; set; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Models/Export/ExportSourceType.cs b/src/Microsoft.Health.Dicom.Core/Models/Export/ExportSourceType.cs deleted file mode 100644 index bd277eb861..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Models/Export/ExportSourceType.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Models.Export; - -/// -/// Specifies the kind of format used to describe the data set to be exported. -/// -public enum ExportSourceType -{ - /// - /// Specifies an unknown source format. - /// - Unknown, - - /// - /// Specifies a list of DICOM identifiers. - /// - Identifiers, -} diff --git a/src/Microsoft.Health.Dicom.Core/Models/Export/ExportSpecification.cs b/src/Microsoft.Health.Dicom.Core/Models/Export/ExportSpecification.cs deleted file mode 100644 index f6ff6683b0..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Models/Export/ExportSpecification.cs +++ /dev/null @@ -1,29 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.ComponentModel.DataAnnotations; - -namespace Microsoft.Health.Dicom.Core.Models.Export; - -/// -/// Represents the desired specification for an export operation, -/// describing both the data to be exported and its destination. -/// -public class ExportSpecification -{ - /// - /// Gets or sets the source of the export operation. - /// - /// The options describing the source. - [Required] - public ExportDataOptions Source { get; set; } - - /// - /// Gets or sets the destination of the export operation. - /// - /// The options describing the destination. - [Required] - public ExportDataOptions Destination { get; set; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Models/Export/IdentifierExportOptions.cs b/src/Microsoft.Health.Dicom.Core/Models/Export/IdentifierExportOptions.cs deleted file mode 100644 index 67aa92ae55..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Models/Export/IdentifierExportOptions.cs +++ /dev/null @@ -1,25 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Globalization; -using Microsoft.Health.Dicom.Core.Models.Common; - -namespace Microsoft.Health.Dicom.Core.Models.Export; - -internal sealed class IdentifierExportOptions : IValidatableObject -{ - public IReadOnlyCollection Values { get; set; } - - public IEnumerable Validate(ValidationContext validationContext) - { - var results = new List(); - if (Values == null || Values.Count == 0) - results.Add(new ValidationResult(string.Format(CultureInfo.CurrentCulture, DicomCoreResource.MissingProperty, nameof(Values)), new string[] { nameof(Values) })); - - return results; - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Models/Export/ReadResult.cs b/src/Microsoft.Health.Dicom.Core/Models/Export/ReadResult.cs deleted file mode 100644 index bb0e2a9433..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Models/Export/ReadResult.cs +++ /dev/null @@ -1,57 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Diagnostics.CodeAnalysis; -using EnsureThat; -using Microsoft.Health.Dicom.Core.Features.Model; - -namespace Microsoft.Health.Dicom.Core.Features.Export; - -/// -/// Represents the result of reading a DICOM study, series, or SOP instance during an export operation. -/// -[SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types", Justification = "Identifiers are not equatable.")] -public readonly struct ReadResult -{ - /// - /// Gets the resolved instance metadata. - /// - /// - /// This value is non- if the read was successful. - /// - /// The successfully read identifier, if successful; otherwise . - public InstanceMetadata Instance { get; } - - /// - /// Gets the failure associated with the read. - /// - /// - /// This value is non- if the read was unsuccessful. - /// - /// The failure that caused the read to fail, if unsuccessful; otherwise . - public ReadFailureEventArgs Failure { get; } - - private ReadResult(InstanceMetadata instance, ReadFailureEventArgs failure) - { - Instance = instance; - Failure = failure; - } - - /// - /// Creates a new instance of the class for an instance identifier. - /// - /// A DICOM instance metadata - /// A successful instance of the structure. - public static ReadResult ForInstance(InstanceMetadata instance) - => new ReadResult(EnsureArg.IsNotNull(instance, nameof(instance)), null); - - /// - /// Creates a new instance of the class for a read failure. - /// - /// The event arguments for a read failure event. - /// An unsuccessful instance of the structure. - public static ReadResult ForFailure(ReadFailureEventArgs args) - => new ReadResult(null, args); -} diff --git a/src/Microsoft.Health.Dicom.Core/Models/HttpWarningCode.cs b/src/Microsoft.Health.Dicom.Core/Models/HttpWarningCode.cs deleted file mode 100644 index d5069c6d2f..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Models/HttpWarningCode.cs +++ /dev/null @@ -1,18 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Models; - -/// -/// Represents Http Warning code. https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Warning#warning_codes -/// -[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1008:Enums should have zero value", Justification = "0 is not valid HttpWarningCode")] -public enum HttpWarningCode -{ - /// - /// Miscellaneous Persistent Warning - /// - MiscPersistentWarning = 299 -} diff --git a/src/Microsoft.Health.Dicom.Core/Models/IgnoreEnumAttribute.cs b/src/Microsoft.Health.Dicom.Core/Models/IgnoreEnumAttribute.cs deleted file mode 100644 index 6bb4decbde..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Models/IgnoreEnumAttribute.cs +++ /dev/null @@ -1,13 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; - -namespace Microsoft.Health.Dicom.Core.Models; - -[AttributeUsage(AttributeTargets.All)] -public sealed class IgnoreEnumAttribute : Attribute -{ -} diff --git a/src/Microsoft.Health.Dicom.Core/Models/IndexStatus.cs b/src/Microsoft.Health.Dicom.Core/Models/IndexStatus.cs deleted file mode 100644 index c8ed719e95..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Models/IndexStatus.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.Health.Dicom.Core.Models; - -/// -/// Representing the index status. -/// -[SuppressMessage("Design", "CA1028:Enum Storage should be Int32", Justification = "Value is stored in SQL as TINYINT.")] -public enum IndexStatus : byte -{ - Creating = 0, - - Created = 1, -} diff --git a/src/Microsoft.Health.Dicom.Core/Models/Operations/DicomOperation.cs b/src/Microsoft.Health.Dicom.Core/Models/Operations/DicomOperation.cs deleted file mode 100644 index 7c758d2d2d..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Models/Operations/DicomOperation.cs +++ /dev/null @@ -1,44 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Models.Operations; - -/// -/// Specifies the category of a DICOM operation. -/// -public enum DicomOperation -{ - /// - /// Specifies an data cleanup operation that cleans up instance data. - /// - [IgnoreEnum] - DataCleanup, - - /// - /// Specifies an content length backfill operation. - /// - [IgnoreEnum] - ContentLengthBackFill, - - /// - /// Specifies an export operation that copies data out of the DICOM server and into an external data store. - /// - Export, - - /// - /// Specifies a reindexing operation that updates the indicies for previously added data based on new tags. - /// - Reindex, - - /// - /// Specifies an operation whose type is missing or unrecognized. - /// - Unknown, - - /// - /// Specifies an update operation that updates the Dicom attributes. - /// - Update -} diff --git a/src/Microsoft.Health.Dicom.Core/Models/Operations/OperationCheckpointState.cs b/src/Microsoft.Health.Dicom.Core/Models/Operations/OperationCheckpointState.cs deleted file mode 100644 index 6c6321c70d..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Models/Operations/OperationCheckpointState.cs +++ /dev/null @@ -1,37 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using Microsoft.Health.Operations; - -namespace Microsoft.Health.Dicom.Core.Models.Operations; - -/// -/// Represents the state of a long-running operation with checkpoint information. -/// -/// The type used to denote the category of operation. -public class OperationCheckpointState -{ - /// - /// Gets or sets the operation ID. - /// - /// The unique ID that denotes a particular operation. - public Guid OperationId { get; init; } - - /// - /// Gets or sets the category of the operation. - /// - public T Type { get; init; } - - /// - /// Gets or sets the execution status of the operation. - /// - public OperationStatus Status { get; init; } - - /// - /// Gets or sets the operation's checkpoint. - /// - public IOperationCheckpoint Checkpoint { get; init; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Models/Operations/OperationQueryCondition.cs b/src/Microsoft.Health.Dicom.Core/Models/Operations/OperationQueryCondition.cs deleted file mode 100644 index 81fec90f92..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Models/Operations/OperationQueryCondition.cs +++ /dev/null @@ -1,42 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using Microsoft.Health.Operations; - -namespace Microsoft.Health.Dicom.Core.Models.Operations; - -/// -/// Represents a set of search criteria when querying for potentially multiple operations. -/// -public sealed class OperationQueryCondition -{ - /// - /// Gets the optional collection of operation types to include in the search results. - /// - /// Zero or more operation types. - public IEnumerable Operations { get; init; } - - /// - /// Gets the optional collection of operation statues to include in the search results. - /// - /// Zero or more statuses. - public IEnumerable Statuses { get; init; } - - /// - /// Gets the optional minimum value for the property - /// to include in the search results. - /// - /// The minimum operation created time if specified; otherwise . - public DateTime CreatedTimeFrom { get; init; } = DateTime.MinValue; - - /// - /// Gets the optional maximum value for the property - /// to include in the search results. - /// - /// The maximum operation created time if specified; otherwise . - public DateTime CreatedTimeTo { get; init; } = DateTime.MaxValue; -} diff --git a/src/Microsoft.Health.Dicom.Core/Models/RequirementCode.cs b/src/Microsoft.Health.Dicom.Core/Models/RequirementCode.cs deleted file mode 100644 index 2b1d1f5c8b..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Models/RequirementCode.cs +++ /dev/null @@ -1,72 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Models; - -/// -/// Service class user (SCU) and service class provider (SCP) requirements. -/// Dicom 3.4.5.4.2.1 -/// -public enum RequirementCode -{ - /// - /// Mandatory for the SCU, and cannot be zero length. - /// - OneOne, - /// - /// Mandatory for the SCU, and can be zero length. The SCP will apply a default. - /// - TwoOne, - /// - /// Mandatory for the SCU, and can be zero length. - /// - TwoTwo, - /// - /// Optional for the SCU, and cannot be zero length. - /// - ThreeOne, - /// - /// Optional for the SCU, and cannot be zero length. - /// - ThreeTwo, - /// - /// Optional for the SCU, and cannot be zero length. - /// - ThreeThree, - /// - /// Special requirement that does not have a code in the table, however, it is still part of the standard. - /// Must not be present. - /// - NotAllowed, - /// - /// Special requirement that does not have a code in the table, however, it is still part of the standard. - /// Can be present but the value must be empty. - /// - MustBeEmpty, - /// - /// Mandatory if certain conditions are met. Cannot be zero length. - /// Given that these conditions cannot be generalized, - /// attributes/sequences with this code are treated as Optional. - /// - OneCOneC, - /// - /// Mandatory if certain conditions are met. Cannot be zero length. - /// Given that these conditions cannot be generalized, - /// attributes/sequences with this code are treated as Optional. - /// - OneCOne, - /// - /// Mandatory if certain conditions are met. Cannot be zero length. - /// Given that these conditions cannot be generalized, - /// attributes/sequences with this code are treated as Optional. - /// - OneCTwo, - /// - /// Mandatory for the SCU if conditions are met. Can be zero length. - /// Given that these conditions cannot be generalized, - /// attributes/sequences with this code are treated as Optional. - /// - TwoCTwoC, -} diff --git a/src/Microsoft.Health.Dicom.Core/Models/SecretKey.cs b/src/Microsoft.Health.Dicom.Core/Models/SecretKey.cs deleted file mode 100644 index 09994f425f..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Models/SecretKey.cs +++ /dev/null @@ -1,27 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Models; - -/// -/// Represents the key for a secret. -/// -public sealed class SecretKey -{ - /// - /// Gets the name of a secret. - /// - /// The name of a secret. - public string Name { get; set; } - - /// - /// Gets the version of a secret. - /// - /// - /// The version only makes sense within the context of a name. - /// - /// The version of a secret associated with a name. - public string Version { get; set; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Models/TimeRange.cs b/src/Microsoft.Health.Dicom.Core/Models/TimeRange.cs deleted file mode 100644 index 4a822a17db..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Models/TimeRange.cs +++ /dev/null @@ -1,50 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; - -namespace Microsoft.Health.Dicom.Core.Models; - -public readonly struct TimeRange : IEquatable -{ - public static readonly TimeRange MaxValue = new TimeRange(DateTimeOffset.MinValue, DateTimeOffset.MaxValue); - - public TimeRange(DateTimeOffset startTime, DateTimeOffset endTime) - { - if (endTime <= startTime) - throw new ArgumentOutOfRangeException(nameof(startTime)); - - Start = startTime; - End = endTime; - } - - public DateTimeOffset Start { get; } - - public DateTimeOffset End { get; } - - public override bool Equals(object obj) - => obj is TimeRange other && Equals(other); - - public bool Equals(TimeRange other) - => Start == other.Start && End == other.End; - - public override int GetHashCode() - => HashCode.Combine(Start, End); - - public override string ToString() - => $"[{Start:O}, {End:O})"; - - public static bool operator ==(TimeRange left, TimeRange right) - => left.Equals(right); - - public static bool operator !=(TimeRange left, TimeRange right) - => !left.Equals(right); - - public static TimeRange After(DateTimeOffset start) - => new TimeRange(start, DateTimeOffset.MaxValue); - - public static TimeRange Before(DateTimeOffset end) - => new TimeRange(DateTimeOffset.MinValue, end); -} diff --git a/src/Microsoft.Health.Dicom.Core/Models/Update/UpdateSpecification.cs b/src/Microsoft.Health.Dicom.Core/Models/Update/UpdateSpecification.cs deleted file mode 100644 index 0c3b6a0a8f..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Models/Update/UpdateSpecification.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using FellowOakDicom; - -namespace Microsoft.Health.Dicom.Core.Models.Update; - -public class UpdateSpecification -{ - public UpdateSpecification(IReadOnlyList studyInstanceUids, DicomDataset changeDataset) - { - StudyInstanceUids = studyInstanceUids; - ChangeDataset = changeDataset; - } - - public IReadOnlyList StudyInstanceUids { get; } - - public DicomDataset ChangeDataset { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Modules/MediationModule.cs b/src/Microsoft.Health.Dicom.Core/Modules/MediationModule.cs deleted file mode 100644 index f8d96aec17..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Modules/MediationModule.cs +++ /dev/null @@ -1,40 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Linq; -using EnsureThat; -using MediatR; -using MediatR.Pipeline; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Health.Extensions.DependencyInjection; - -namespace Microsoft.Health.Dicom.Core.Modules; - -public class MediationModule : IStartupModule -{ - /// - public void Load(IServiceCollection services) - { - EnsureArg.IsNotNull(services, nameof(services)); - - services.AddMediatR(c => c.RegisterServicesFromAssemblyContaining()); - services.AddTransient(typeof(IPipelineBehavior<,>), typeof(RequestPreProcessorBehavior<,>)); - services.AddTransient(typeof(IPipelineBehavior<,>), typeof(RequestPostProcessorBehavior<,>)); - - services.TypesInSameAssemblyAs() - .Transient() - .AsImplementedInterfaces(IsPipelineBehavior); - - var openRequestInterfaces = new Type[] { typeof(IRequestHandler<,>) }; - - services.TypesInSameAssemblyAs() - .Where(y => y.Type.IsGenericType && openRequestInterfaces.Contains(y.Type.GetGenericTypeDefinition())) - .Transient(); - } - - private static bool IsPipelineBehavior(Type t) - => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IPipelineBehavior<,>); -} diff --git a/src/Microsoft.Health.Dicom.Core/Modules/ServiceModule.cs b/src/Microsoft.Health.Dicom.Core/Modules/ServiceModule.cs deleted file mode 100644 index 7b4a4711da..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Modules/ServiceModule.cs +++ /dev/null @@ -1,285 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Health.Core.Features.Identity; -using Microsoft.Health.Dicom.Core.Features.ChangeFeed; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Delete; -using Microsoft.Health.Dicom.Core.Features.Export; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Features.Indexing; -using Microsoft.Health.Dicom.Core.Features.Operations; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Core.Features.Query; -using Microsoft.Health.Dicom.Core.Features.Query.Model; -using Microsoft.Health.Dicom.Core.Features.Retrieve; -using Microsoft.Health.Dicom.Core.Features.Store; -using Microsoft.Health.Dicom.Core.Features.Store.Entries; -using Microsoft.Health.Dicom.Core.Features.Telemetry; -using Microsoft.Health.Dicom.Core.Features.Update; -using Microsoft.Health.Dicom.Core.Features.Validation; -using Microsoft.Health.Dicom.Core.Features.Workitem; -using Microsoft.Health.Extensions.DependencyInjection; - -namespace Microsoft.Health.Dicom.Core.Modules; - -public class ServiceModule : IStartupModule -{ - public void Load(IServiceCollection services) - { - EnsureArg.IsNotNull(services, nameof(services)); - - services.AddFellowOakDicomServices(skipValidation: true); - - services.Add() - .Singleton() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Scoped() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Scoped() - .AsSelf() - .AsImplementedInterfaces() - .AsFactory(); - - services.Add() - .Scoped() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Singleton() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Singleton() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Singleton() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Transient() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Scoped() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Scoped() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Scoped() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Transient() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Transient() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Transient() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Scoped() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Singleton() - .AsSelf() - .AsImplementedInterfaces(); - - services.AddTransient, QueryParser>(); - services.AddTransient, WorkitemQueryParser>(); - - services.Add() - .Scoped() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Scoped() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Singleton() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Scoped() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Scoped() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Scoped() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Scoped() - .AsSelf() - .AsImplementedInterfaces(); - - services.AddSingleton(GuidFactory.Default); - - services.AddScoped(); - - services.AddSingleton(); - - services.Add() - .Scoped() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Singleton() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Singleton() - .AsSelf() - .AsImplementedInterfaces(); - - services.AddExternalCredentialProvider(); - - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - - AddExtendedQueryTagServices(services); - - AddWorkItemServices(services); - - AddExportServices(services); - } - - private static void AddExtendedQueryTagServices(IServiceCollection services) - { - services.Add() - .Singleton() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Scoped() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Scoped() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Scoped() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Scoped() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Scoped() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Scoped() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Scoped() - .AsSelf() - .AsImplementedInterfaces(); - } - - private static void AddWorkItemServices(IServiceCollection services) - { - services.Add() - .Scoped() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Scoped() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Scoped() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Scoped() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Scoped() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Scoped() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Scoped() - .AsSelf() - .AsImplementedInterfaces(); - - services.Add() - .Scoped() - .AsSelf() - .AsImplementedInterfaces(); - } - - private static void AddExportServices(IServiceCollection services) - { - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.TryAddEnumerable(ServiceDescriptor.Scoped()); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Properties/AssemblyInfo.cs b/src/Microsoft.Health.Dicom.Core/Properties/AssemblyInfo.cs deleted file mode 100644 index e74a955395..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,25 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Resources; -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] -[assembly: InternalsVisibleTo("Microsoft.Health.Dicom.Api.UnitTests")] -[assembly: InternalsVisibleTo("Microsoft.Health.Dicom.Blob")] -[assembly: InternalsVisibleTo("Microsoft.Health.Dicom.Blob.UnitTests")] -[assembly: InternalsVisibleTo("Microsoft.Health.Dicom.Core.UnitTests")] -[assembly: InternalsVisibleTo("Microsoft.Health.Dicom.Tests.Common")] -[assembly: InternalsVisibleTo("Microsoft.Health.Dicom.Functions")] -[assembly: InternalsVisibleTo("Microsoft.Health.Dicom.Functions.Client")] -[assembly: InternalsVisibleTo("Microsoft.Health.Dicom.Functions.Client.UnitTests")] -[assembly: InternalsVisibleTo("Microsoft.Health.Dicom.Functions.UnitTests")] -[assembly: InternalsVisibleTo("Microsoft.Health.Dicom.SqlServer")] -[assembly: InternalsVisibleTo("Microsoft.Health.Dicom.SqlServer.UnitTests")] -[assembly: InternalsVisibleTo("Microsoft.Health.Dicom.Tests.Integration")] -[assembly: InternalsVisibleTo("Microsoft.Health.Dicom.Web.Tests.E2E")] -[assembly: NeutralResourcesLanguage("en-us")] -[assembly: CLSCompliant(false)] diff --git a/src/Microsoft.Health.Dicom.Core/Registration/FellowOakServiceExtensions.cs b/src/Microsoft.Health.Dicom.Core/Registration/FellowOakServiceExtensions.cs deleted file mode 100644 index 04aacb0f1d..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Registration/FellowOakServiceExtensions.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using FellowOakDicom; -using FellowOakDicom.Imaging; -using FellowOakDicom.Imaging.NativeCodec; - -namespace Microsoft.Extensions.DependencyInjection; - -internal static class FellowOakServiceExtensions -{ - public static IServiceCollection AddFellowOakDicomServices(this IServiceCollection services, bool skipValidation = false) - { - if (skipValidation) - { - // Note: this is an extension method, but it isn't stateful. - // Instead it modifies a static property, so we'll change the invocation to look more appropriate - DicomValidationBuilderExtension.SkipValidation(null); - } - - services - .AddFellowOakDicom() - .AddTranscoderManager() - .AddImageManager(); - - return services; - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Registration/IDicomFunctionsBuilder.cs b/src/Microsoft.Health.Dicom.Core/Registration/IDicomFunctionsBuilder.cs deleted file mode 100644 index ee55d7e78e..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Registration/IDicomFunctionsBuilder.cs +++ /dev/null @@ -1,16 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.Health.Dicom.Core.Registration; - -/// -/// A builder type for configuring DICOM function services. -/// -public interface IDicomFunctionsBuilder -{ - IServiceCollection Services { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Registration/IDicomServerBuilder.cs b/src/Microsoft.Health.Dicom.Core/Registration/IDicomServerBuilder.cs deleted file mode 100644 index 3f72a9a4b2..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Registration/IDicomServerBuilder.cs +++ /dev/null @@ -1,16 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.Health.Dicom.Core.Registration; - -/// -/// A builder type for configuring DICOM server services. -/// -public interface IDicomServerBuilder -{ - IServiceCollection Services { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Registration/RecyclableMemoryStreamRegistrationExtensions.cs b/src/Microsoft.Health.Dicom.Core/Registration/RecyclableMemoryStreamRegistrationExtensions.cs deleted file mode 100644 index bfa7a21bbf..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Registration/RecyclableMemoryStreamRegistrationExtensions.cs +++ /dev/null @@ -1,46 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using EnsureThat; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Microsoft.IO; - -namespace Microsoft.Health.Dicom.Core.Registration; - -public static class RecyclableMemoryStreamRegistrationExtensions -{ - public const string DefaultSectionName = "RecyclableMemoryStream"; - - public static IServiceCollection AddRecyclableMemoryStreamManager(this IServiceCollection services, IConfiguration configuration) - { - EnsureArg.IsNotNull(configuration, nameof(configuration)); - return services.AddRecyclableMemoryStreamManager( - o => configuration - .GetSection(DefaultSectionName) - .Bind(o)); - } - - public static IServiceCollection AddRecyclableMemoryStreamManager(this IServiceCollection services, Action configure) - { - EnsureArg.IsNotNull(configure, nameof(configure)); - return services - .AddRecyclableMemoryStreamManager() - .Configure(configure); - } - - public static IServiceCollection AddRecyclableMemoryStreamManager(this IServiceCollection services) - { - EnsureArg.IsNotNull(services, nameof(services)); - return services.AddSingleton( - sp => - { - RecyclableMemoryStreamManager.Options options = sp.GetRequiredService>().Value; - return new RecyclableMemoryStreamManager(options); - }); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Serialization/DicomIdentifierJsonConverter.cs b/src/Microsoft.Health.Dicom.Core/Serialization/DicomIdentifierJsonConverter.cs deleted file mode 100644 index 71403ff5dd..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Serialization/DicomIdentifierJsonConverter.cs +++ /dev/null @@ -1,21 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Text.Json; -using System.Text.Json.Serialization; -using EnsureThat; -using Microsoft.Health.Dicom.Core.Models.Common; - -namespace Microsoft.Health.Dicom.Core.Serialization; - -internal sealed class DicomIdentifierJsonConverter : JsonConverter -{ - public override DicomIdentifier Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - => DicomIdentifier.Parse(reader.GetString()); - - public override void Write(Utf8JsonWriter writer, DicomIdentifier value, JsonSerializerOptions options) - => EnsureArg.IsNotNull(writer, nameof(writer)).WriteStringValue(value.ToString()); -} diff --git a/src/Microsoft.Health.Dicom.Core/Serialization/ExportDataOptionsJsonConverter.T.cs b/src/Microsoft.Health.Dicom.Core/Serialization/ExportDataOptionsJsonConverter.T.cs deleted file mode 100644 index cb3ceff3ac..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Serialization/ExportDataOptionsJsonConverter.T.cs +++ /dev/null @@ -1,51 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Text.Json; -using System.Text.Json.Serialization; -using EnsureThat; -using Microsoft.Health.Dicom.Core.Models.Export; - -namespace Microsoft.Health.Dicom.Core.Serialization; - -internal sealed class ExportDataOptionsJsonConverter : JsonConverter> -{ - private readonly Func _getType; - - public ExportDataOptionsJsonConverter(Func getType) - => _getType = EnsureArg.IsNotNull(getType, nameof(getType)); - - public override ExportDataOptions Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - ExportDataOptions intermediate = JsonSerializer.Deserialize(ref reader, options); - - Type type = _getType(intermediate.Type); - return new ExportDataOptions(intermediate.Type, intermediate.Settings.Deserialize(type, options)); - } - - public override void Write(Utf8JsonWriter writer, ExportDataOptions value, JsonSerializerOptions options) - { - writer.WriteStartObject(); - - writer.WritePropertyName(GetPropertyName(nameof(ExportDataOptions.Type), options.PropertyNamingPolicy)); - JsonSerializer.Serialize(writer, value.Type, options); - - writer.WritePropertyName(GetPropertyName(nameof(ExportDataOptions.Settings), options.PropertyNamingPolicy)); - JsonSerializer.Serialize(writer, value.Settings, _getType(value.Type), options); - - writer.WriteEndObject(); - } - - private static string GetPropertyName(string name, JsonNamingPolicy policy) - => policy != null ? policy.ConvertName(name) : name; - - private sealed class ExportDataOptions - { - public T Type { get; set; } - - public JsonElement Settings { get; set; } - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Serialization/ExportDataOptionsJsonConverter.cs b/src/Microsoft.Health.Dicom.Core/Serialization/ExportDataOptionsJsonConverter.cs deleted file mode 100644 index edf5d44f2f..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Serialization/ExportDataOptionsJsonConverter.cs +++ /dev/null @@ -1,55 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Globalization; -using System.Text.Json; -using System.Text.Json.Serialization; -using EnsureThat; -using Microsoft.Health.Dicom.Core.Models.Export; - -namespace Microsoft.Health.Dicom.Core.Serialization; - -internal sealed class ExportDataOptionsJsonConverter : JsonConverterFactory -{ - public override bool CanConvert(Type typeToConvert) - => typeToConvert.IsGenericType && typeToConvert.GetGenericTypeDefinition() == typeof(ExportDataOptions<>); - - public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) - { - Type arg = EnsureArg.IsNotNull(typeToConvert, nameof(typeToConvert)).GetGenericArguments()[0]; - if (arg == typeof(ExportSourceType)) - return new ExportDataOptionsJsonConverter(MapSourceType); - else if (arg == typeof(ExportDestinationType)) - return new ExportDataOptionsJsonConverter(MapDestinationType); - else - throw new JsonException( - string.Format(CultureInfo.CurrentCulture, DicomCoreResource.InvalidType, typeToConvert)); - } - - private static Type MapSourceType(ExportSourceType type) - => type switch - { - ExportSourceType.Identifiers => typeof(IdentifierExportOptions), - _ => throw new JsonException( - string.Format( - CultureInfo.CurrentCulture, - DicomCoreResource.UnexpectedValue, - nameof(ExportDataOptions.Type), - nameof(ExportSourceType.Identifiers))), - }; - - private static Type MapDestinationType(ExportDestinationType type) - => type switch - { - ExportDestinationType.AzureBlob => typeof(AzureBlobExportOptions), - _ => throw new JsonException( - string.Format( - CultureInfo.CurrentCulture, - DicomCoreResource.UnexpectedValue, - nameof(ExportDataOptions.Type), - nameof(ExportDestinationType.AzureBlob))), - }; -} diff --git a/src/Microsoft.Health.Dicom.Core/Serialization/Newtonsoft/DicomIdentifierJsonConverter.cs b/src/Microsoft.Health.Dicom.Core/Serialization/Newtonsoft/DicomIdentifierJsonConverter.cs deleted file mode 100644 index 93a07417bc..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Serialization/Newtonsoft/DicomIdentifierJsonConverter.cs +++ /dev/null @@ -1,27 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Globalization; -using Microsoft.Health.Dicom.Core.Models.Common; -using Newtonsoft.Json; - -namespace Microsoft.Health.Dicom.Core.Serialization.Newtonsoft; - -internal sealed class DicomIdentifierJsonConverter : JsonConverter -{ - public override DicomIdentifier ReadJson(JsonReader reader, Type objectType, DicomIdentifier existingValue, bool hasExistingValue, JsonSerializer serializer) - => reader.TokenType == JsonToken.String - ? DicomIdentifier.Parse(reader.Value as string) - : throw new JsonException( - string.Format( - CultureInfo.CurrentCulture, - DicomCoreResource.UnexpectedJsonToken, - JsonToken.String, - reader.TokenType)); - - public override void WriteJson(JsonWriter writer, DicomIdentifier value, JsonSerializer serializer) - => writer.WriteValue(value.ToString()); -} diff --git a/src/Microsoft.Health.Dicom.Core/Serialization/Newtonsoft/ExportDataOptionsJsonConverter.cs b/src/Microsoft.Health.Dicom.Core/Serialization/Newtonsoft/ExportDataOptionsJsonConverter.cs deleted file mode 100644 index eebe4a9ddd..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Serialization/Newtonsoft/ExportDataOptionsJsonConverter.cs +++ /dev/null @@ -1,101 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Globalization; -using EnsureThat; -using Microsoft.Health.Dicom.Core.Models.Export; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Newtonsoft.Json.Serialization; - -namespace Microsoft.Health.Dicom.Core.Serialization.Newtonsoft; - -internal sealed class ExportSourceOptionsJsonConverter : ExportDataOptionsJsonConverter -{ - public ExportSourceOptionsJsonConverter(NamingStrategy namingStrategy) - : base(MapSourceType, namingStrategy) - { } - - private static Type MapSourceType(ExportSourceType type) - => type switch - { - ExportSourceType.Identifiers => typeof(IdentifierExportOptions), - _ => throw new JsonException( - string.Format( - CultureInfo.CurrentCulture, - DicomCoreResource.UnexpectedValue, - nameof(ExportDataOptions.Type), - nameof(ExportSourceType.Identifiers))), - }; -} - -internal sealed class ExportDestinationOptionsJsonConverter : ExportDataOptionsJsonConverter -{ - public ExportDestinationOptionsJsonConverter(NamingStrategy namingStrategy) - : base(MapDestinationType, namingStrategy) - { } - - private static Type MapDestinationType(ExportDestinationType type) - => type switch - { - ExportDestinationType.AzureBlob => typeof(AzureBlobExportOptions), - _ => throw new JsonException( - string.Format( - CultureInfo.CurrentCulture, - DicomCoreResource.UnexpectedValue, - nameof(ExportDataOptions.Type), - nameof(ExportDestinationType.AzureBlob))), - }; -} - -internal abstract class ExportDataOptionsJsonConverter : JsonConverter> -{ - private readonly Func _getType; - private readonly NamingStrategy _namingStrategy; - - protected ExportDataOptionsJsonConverter(Func getType, NamingStrategy namingStrategy) - { - _getType = EnsureArg.IsNotNull(getType, nameof(getType)); - _namingStrategy = EnsureArg.IsNotNull(namingStrategy, nameof(namingStrategy)); - } - - public override ExportDataOptions ReadJson(JsonReader reader, Type objectType, ExportDataOptions existingValue, bool hasExistingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.Null) - return null; - - ExportDataOptions intermediate = serializer.Deserialize(reader); - - Type type = _getType(intermediate.Type); - return new ExportDataOptions(intermediate.Type, intermediate.Settings.ToObject(type, serializer)); - } - - public override void WriteJson(JsonWriter writer, ExportDataOptions value, JsonSerializer serializer) - { - if (value == null) - { - writer.WriteNull(); - return; - } - - writer.WriteStartObject(); - - writer.WritePropertyName(_namingStrategy.GetPropertyName(nameof(ExportDataOptions.Type), false)); - serializer.Serialize(writer, value.Type); - - writer.WritePropertyName(_namingStrategy.GetPropertyName(nameof(ExportDataOptions.Settings), false)); - serializer.Serialize(writer, value.Settings); - - writer.WriteEndObject(); - } - - private sealed class ExportDataOptions - { - public T Type { get; set; } - - public JObject Settings { get; set; } - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Serialization/StrictStringEnumConverter.T.cs b/src/Microsoft.Health.Dicom.Core/Serialization/StrictStringEnumConverter.T.cs deleted file mode 100644 index 2318d5c43b..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Serialization/StrictStringEnumConverter.T.cs +++ /dev/null @@ -1,71 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Globalization; -using System.Linq; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Microsoft.Health.Dicom.Core.Serialization; - -// Note: This class ignore the JsonNamingPolicy as it is not currently in-use for DICOM. -internal sealed class StrictStringEnumConverter : JsonConverter - where T : struct, Enum -{ - private readonly JsonNamingPolicy _namingPolicy; - - private static readonly ImmutableDictionary Values = ImmutableDictionary.CreateRange( - StringComparer.OrdinalIgnoreCase, - Enum.GetValues().Select(x => KeyValuePair.Create(Enum.GetName(x), x))); - - /// - /// Creates a new instance of the - /// with the given naming policy. - /// - /// An optional JSON naming policy. - public StrictStringEnumConverter(JsonNamingPolicy namingPolicy = null) - => _namingPolicy = namingPolicy; - - public override bool CanConvert(Type typeToConvert) - => typeToConvert == typeof(T); - - public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType != JsonTokenType.String) - { - throw new JsonException( - string.Format(CultureInfo.CurrentCulture, DicomCoreResource.UnexpectedJsonToken, JsonTokenType.String, reader.TokenType)); - } - - string name = reader.GetString(); - if (!Values.TryGetValue(name, out T value)) - { - throw new JsonException( - string.Format(CultureInfo.CurrentCulture, DicomCoreResource.UnexpectedValue, name, GetOrderedNames())); - } - - return value; - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - if (!Enum.IsDefined(value)) - { - throw new JsonException( - string.Format(CultureInfo.CurrentCulture, DicomCoreResource.UnexpectedValue, value, GetOrderedNames())); - } - - writer.WriteStringValue(ConvertName(Enum.GetName(value))); - } - - private string ConvertName(string name) - => _namingPolicy != null ? _namingPolicy.ConvertName(name) : name; - - private static string GetOrderedNames() - => string.Join(", ", Values.Keys.OrderBy(x => x, StringComparer.OrdinalIgnoreCase).Select(x => $"'{x}'")); -} diff --git a/src/Microsoft.Health.Dicom.Core/Serialization/StrictStringEnumConverter.cs b/src/Microsoft.Health.Dicom.Core/Serialization/StrictStringEnumConverter.cs deleted file mode 100644 index 62f963d043..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Serialization/StrictStringEnumConverter.cs +++ /dev/null @@ -1,45 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Text.Json; -using System.Text.Json.Serialization; -using EnsureThat; - -namespace Microsoft.Health.Dicom.Core.Serialization; - -/// -/// Represents a for enumeration types where values are -/// strictly represented by their names as JSON string tokens. -/// -public sealed class StrictStringEnumConverter : JsonConverterFactory -{ - private readonly JsonNamingPolicy _namingPolicy; - - /// - /// Creates a new instance of the - /// with the given naming policy. - /// - /// An optional JSON naming policy. - public StrictStringEnumConverter(JsonNamingPolicy namingPolicy = null) - => _namingPolicy = namingPolicy; - - /// - /// Determines whether the JSON converter can operate on the given type. - /// - /// The type to serialize and/or deserialize. - /// - /// if the type is compatible with the converter; otherwise . - /// - /// is . - public override bool CanConvert(Type typeToConvert) - => EnsureArg.IsNotNull(typeToConvert).IsEnum; - - /// - public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) - => Activator.CreateInstance( - typeof(StrictStringEnumConverter<>).MakeGenericType(EnsureArg.IsNotNull(typeToConvert)), - new object[] { _namingPolicy }) as JsonConverter; -} diff --git a/src/Microsoft.Health.Dicom.Core/Web/AcceptHeaderParameterNames.cs b/src/Microsoft.Health.Dicom.Core/Web/AcceptHeaderParameterNames.cs deleted file mode 100644 index fad69821ef..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Web/AcceptHeaderParameterNames.cs +++ /dev/null @@ -1,13 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Web; - -public static class AcceptHeaderParameterNames -{ - public const string Type = "type"; - public const string TransferSyntax = "transfer-syntax"; - public const string Quality = "q"; -} diff --git a/src/Microsoft.Health.Dicom.Core/Web/DicomTransferSyntaxUids.cs b/src/Microsoft.Health.Dicom.Core/Web/DicomTransferSyntaxUids.cs deleted file mode 100644 index 7ccb8a85b8..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Web/DicomTransferSyntaxUids.cs +++ /dev/null @@ -1,29 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using EnsureThat; -using FellowOakDicom; - -namespace Microsoft.Health.Dicom.Core; - -public static class DicomTransferSyntaxUids -{ - public const string Original = "*"; - public const string ExplicitVRLittleEndian = "1.2.840.10008.1.2.1"; - - public static bool IsOriginalTransferSyntaxRequested(string transferSyntax) - { - return Original.Equals(transferSyntax, StringComparison.Ordinal); - } - - public static bool AreEqual(string transferSyntaxA, string transferSyntaxB) - { - EnsureArg.IsNotNull(transferSyntaxA, nameof(transferSyntaxA)); - EnsureArg.IsNotNull(transferSyntaxB, nameof(transferSyntaxB)); - - return DicomTransferSyntax.Parse(transferSyntaxA) == DicomTransferSyntax.Parse(transferSyntaxB); - } -} diff --git a/src/Microsoft.Health.Dicom.Core/Web/IMultipartReader.cs b/src/Microsoft.Health.Dicom.Core/Web/IMultipartReader.cs deleted file mode 100644 index e13ce93a2c..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Web/IMultipartReader.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.Health.Dicom.Core.Web; - -/// -/// Provides functionalities to read multipart message. -/// -public interface IMultipartReader -{ - /// - /// Read the next body part of a multipart message. - /// - /// The cancellation token. - /// An instance of representing the read body part. - Task ReadNextBodyPartAsync(CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Core/Web/IMultipartReaderFactory.cs b/src/Microsoft.Health.Dicom.Core/Web/IMultipartReaderFactory.cs deleted file mode 100644 index a1fb813397..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Web/IMultipartReaderFactory.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.IO; - -namespace Microsoft.Health.Dicom.Core.Web; - -/// -/// Provides functionality to create a new instance of . -/// -public interface IMultipartReaderFactory -{ - /// - /// Creates a new instance of . - /// - /// The request content type. - /// The request body. - /// An instance of . - IMultipartReader Create(string contentType, Stream body); -} diff --git a/src/Microsoft.Health.Dicom.Core/Web/ISeekableStreamConverter.cs b/src/Microsoft.Health.Dicom.Core/Web/ISeekableStreamConverter.cs deleted file mode 100644 index cb19d59889..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Web/ISeekableStreamConverter.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.Health.Dicom.Core.Web; - -/// -/// Provides functionality to convert stream into a seekable stream. -/// -public interface ISeekableStreamConverter -{ - /// - /// Converts the into a seekable stream. - /// - /// The stream to convert. - /// The cancellation token. - /// A instance of seekable . - Task ConvertAsync(Stream stream, CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Core/Web/KnownContentTypes.cs b/src/Microsoft.Health.Dicom.Core/Web/KnownContentTypes.cs deleted file mode 100644 index 6b22c34ffc..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Web/KnownContentTypes.cs +++ /dev/null @@ -1,28 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Web; - -public static class KnownContentTypes -{ - public const string ApplicationDicom = "application/dicom"; - public const string ApplicationDicomJson = "application/dicom+json"; - public const string ApplicationOctetStream = "application/octet-stream"; - public const string ApplicationJson = "application/json"; - public const string ApplicationJsonUtf8 = "application/json; charset=utf-8"; - public const string ImageJpeg = "image/jpeg"; - public const string ImagePng = "image/png"; - public const string MultipartRelated = "multipart/related"; - public const string ImageJpeg2000 = "image/jp2"; - public const string ImageDicomRle = "image/dicom-rle"; - public const string ImageJpegLs = "image/jls"; - public const string ImageJpeg2000Part2 = "image/jpx"; - public const string VideoMpeg2 = "video/mpeg2"; - public const string VideoMp4 = "video/mp4"; - public const string AnyMediaType = "*/*"; - public const string TransferSyntax = "transfer-syntax"; - public const string Boundary = "boundary"; - public const string ContentType = "Content-Type"; -} diff --git a/src/Microsoft.Health.Dicom.Core/Web/MultipartBodyPart.cs b/src/Microsoft.Health.Dicom.Core/Web/MultipartBodyPart.cs deleted file mode 100644 index a4fd812687..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Web/MultipartBodyPart.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.IO; -using EnsureThat; - -namespace Microsoft.Health.Dicom.Core.Web; - -public class MultipartBodyPart -{ - public MultipartBodyPart(string contentType, Stream seekableStream) - { - EnsureArg.IsNotNull(seekableStream, nameof(seekableStream)); - - ContentType = contentType; - SeekableStream = seekableStream; - } - - public string ContentType { get; } - - public Stream SeekableStream { get; } -} diff --git a/src/Microsoft.Health.Dicom.Core/Web/OtherHeaderParameterNames.cs b/src/Microsoft.Health.Dicom.Core/Web/OtherHeaderParameterNames.cs deleted file mode 100644 index 62803171d3..0000000000 --- a/src/Microsoft.Health.Dicom.Core/Web/OtherHeaderParameterNames.cs +++ /dev/null @@ -1,11 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Core.Web; - -public static class OtherHeaderParameterNames -{ - public const string RequestOriginal = "msdicom-request-original"; -} diff --git a/src/Microsoft.Health.Dicom.Functions.Abstractions.UnitTests/Export/ExportCheckpointTests.cs b/src/Microsoft.Health.Dicom.Functions.Abstractions.UnitTests/Export/ExportCheckpointTests.cs deleted file mode 100644 index fed889e2b3..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Abstractions.UnitTests/Export/ExportCheckpointTests.cs +++ /dev/null @@ -1,29 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using Microsoft.Health.Dicom.Functions.Export; -using Xunit; - -namespace Microsoft.Health.Dicom.Functions.UnitTests.Export; - -public class ExportCheckpointTests -{ - [Fact] - public void GivenCheckpoint_WhenRetrievingAdditionalProperties_ThenGetOperationSpecificValues() - { - var checkpoint = new ExportCheckpoint - { - ErrorHref = new Uri("http://storage-acccount/errors.json"), - Progress = new ExportProgress(1234, 5), - }; - - ExportResults results = checkpoint.GetResults(null) as ExportResults; - Assert.NotNull(results); - Assert.Equal(1234, results.Exported); - Assert.Equal(5, results.Skipped); - Assert.Equal(new Uri("http://storage-acccount/errors.json"), results.ErrorHref); - } -} diff --git a/src/Microsoft.Health.Dicom.Functions.Abstractions.UnitTests/Export/ExportProgressTests.cs b/src/Microsoft.Health.Dicom.Functions.Abstractions.UnitTests/Export/ExportProgressTests.cs deleted file mode 100644 index f006011949..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Abstractions.UnitTests/Export/ExportProgressTests.cs +++ /dev/null @@ -1,25 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Microsoft.Health.Dicom.Functions.Export; -using Xunit; - -namespace Microsoft.Health.Dicom.Functions.UnitTests.Export; - -public class ExportProgressTests -{ - [Theory] - [InlineData(0, 0, 1, 2)] - [InlineData(1, 2, 3, 4)] - public void GivenPartialProgress_WhenAdding_ThenAddPropertiesCorrectly(long success1, long skipped1, long success2, long skipped2) - { - var x = new ExportProgress(success1, skipped1); - var y = new ExportProgress(success2, skipped2); - - ExportProgress actual = x + y; - Assert.Equal(success1 + success2, actual.Exported); - Assert.Equal(skipped1 + skipped2, actual.Skipped); - } -} diff --git a/src/Microsoft.Health.Dicom.Functions.Abstractions.UnitTests/Indexing/ReindexCheckpointTests.cs b/src/Microsoft.Health.Dicom.Functions.Abstractions.UnitTests/Indexing/ReindexCheckpointTests.cs deleted file mode 100644 index cd52d4ea51..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Abstractions.UnitTests/Indexing/ReindexCheckpointTests.cs +++ /dev/null @@ -1,41 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Globalization; -using System.Linq; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Functions.Indexing; -using Xunit; - -namespace Microsoft.Health.Dicom.Functions.UnitTests.Indexing; - -public class ReindexCheckpointTests -{ - [Fact] - public void GivenEmptyInput_WhenGettingPercentComplete_ThenReturnZero() - => Assert.Equal(0, new ReindexCheckpoint().PercentComplete); - - [Fact] - public void GivenEmptyInput_WhenGettingResourceIds_ThenReturnZero() - => Assert.Null(new ReindexCheckpoint().ResourceIds); - - [Theory] - [InlineData(4, 4, 25)] - [InlineData(3, 4, 50)] - [InlineData(2, 4, 75)] - [InlineData(1, 4, 100)] - [InlineData(1, 1, 100)] - public void GivenReindexInput_WhenGettingPercentComplete_ThenReturnComputedProgress(int start, int end, int expected) - => Assert.Equal(expected, new ReindexCheckpoint { Completed = new WatermarkRange(start, end) }.PercentComplete); - - [Fact] - public void GivenReindexInput_WhenGettingResourceIds_ThenReturnConvertedIds() - { - int[] expectedTagKeys = new int[] { 1, 3, 10 }; - var input = new ReindexCheckpoint { QueryTagKeys = expectedTagKeys }; - Assert.True(input.ResourceIds.SequenceEqual(expectedTagKeys.Select(x => x.ToString(CultureInfo.InvariantCulture)))); - } -} diff --git a/src/Microsoft.Health.Dicom.Functions.Abstractions.UnitTests/Microsoft.Health.Dicom.Functions.Abstractions.UnitTests.csproj b/src/Microsoft.Health.Dicom.Functions.Abstractions.UnitTests/Microsoft.Health.Dicom.Functions.Abstractions.UnitTests.csproj deleted file mode 100644 index a2e2196ae2..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Abstractions.UnitTests/Microsoft.Health.Dicom.Functions.Abstractions.UnitTests.csproj +++ /dev/null @@ -1,22 +0,0 @@ - - - - false - Microsoft.Health.Dicom.Functions.UnitTests - $(LibraryFrameworks) - - - - - - - - - - - - - - - - diff --git a/src/Microsoft.Health.Dicom.Functions.Abstractions.UnitTests/Update/UpdateCheckpointTests.cs b/src/Microsoft.Health.Dicom.Functions.Abstractions.UnitTests/Update/UpdateCheckpointTests.cs deleted file mode 100644 index 4f3442bd8b..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Abstractions.UnitTests/Update/UpdateCheckpointTests.cs +++ /dev/null @@ -1,45 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Linq; -using Microsoft.Health.Dicom.Functions.Update; -using Xunit; - -namespace Microsoft.Health.Dicom.Functions.UnitTests.Update; - -public class UpdateCheckpointTests -{ - [Fact] - public void GivenEmptyInput_WhenGettingPercentComplete_ThenReturnZero() - => Assert.Equal(0, new UpdateCheckpoint().PercentComplete); - - [Theory] - [InlineData(4, 4, 100)] - [InlineData(4, 3, 75)] - [InlineData(4, 2, 50)] - [InlineData(4, 0, 0)] - public void GivenUpdateInput_WhenGettingPercentComplete_ThenReturnComputedProgress(int total, int processed, int expected) - => Assert.Equal(expected, new UpdateCheckpoint { StudyInstanceUids = Enumerable.Repeat(".", total).ToList(), NumberOfStudyProcessed = processed }.PercentComplete); - - [Fact] - public void GivenCheckpoint_WhenRetrievingAdditionalProperties_ThenGetOperationSpecificValues() - { - var checkpoint = new UpdateCheckpoint - { - NumberOfStudyProcessed = 5, - NumberOfStudyCompleted = 4, - TotalNumberOfInstanceUpdated = 20, - Errors = new List() - }; - - UpdateResult results = checkpoint.GetResults(null) as UpdateResult; - Assert.NotNull(results); - Assert.Equal(20, results.InstanceUpdated); - Assert.Equal(5, results.StudyProcessed); - Assert.Equal(4, results.StudyUpdated); - Assert.Equal(new List(), results.Errors); - } -} diff --git a/src/Microsoft.Health.Dicom.Functions.Abstractions/BatchingOptions.cs b/src/Microsoft.Health.Dicom.Functions.Abstractions/BatchingOptions.cs deleted file mode 100644 index 640397957a..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Abstractions/BatchingOptions.cs +++ /dev/null @@ -1,46 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.ComponentModel.DataAnnotations; -using Newtonsoft.Json; - -namespace Microsoft.Health.Dicom.Functions; - -/// -/// Represents options for activity batching frequently used by "fan-out/fan-in" scenarios. -/// -public sealed class BatchingOptions -{ - /// - /// Gets or sets the size of each batch. - /// - /// - /// A batch typically represents the input to an activity. - /// - /// The number of elements in each batch. - [Range(1, int.MaxValue)] - public int Size { get; set; } - - /// - /// Gets or sets the maximum number of concurrent batches processed at a given time. - /// - /// - /// This typically represents the maximum number of concurrent activities. - /// - /// The maximum number of batches that may be processed at once. - [Range(1, int.MaxValue)] - public int MaxParallelCount { get; set; } - - /// - /// Gets the maximum number of elements that are processed concurrently. - /// - /// - /// The value of the property multiplied by the - /// value of the property. - /// - [JsonIgnore] - public int MaxParallelElements => Size * MaxParallelCount; -} diff --git a/src/Microsoft.Health.Dicom.Functions.Abstractions/ContentLengthBackFillhCheckpoint/ContentLengthCheckpointCheckPoint.cs b/src/Microsoft.Health.Dicom.Functions.Abstractions/ContentLengthBackFillhCheckpoint/ContentLengthCheckpointCheckPoint.cs deleted file mode 100644 index 867884c63a..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Abstractions/ContentLengthBackFillhCheckpoint/ContentLengthCheckpointCheckPoint.cs +++ /dev/null @@ -1,35 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Operations.Functions.DurableTask; -using Newtonsoft.Json.Linq; - -namespace Microsoft.Health.Dicom.Functions.ContentLengthBackFill; - -public class ContentLengthBackFillCheckPoint : ContentLengthBackFillInput, IOrchestrationCheckpoint -{ - public WatermarkRange? Completed { get; set; } - - public DateTime? CreatedTime { get; set; } - - public int? PercentComplete - { - get - { - if (Completed.HasValue) - { - WatermarkRange range = Completed.GetValueOrDefault(); - return range.End == 1 ? 100 : (int)((double)(range.End - range.Start + 1) / range.End * 100); - } - return 0; - } - } - public IReadOnlyCollection ResourceIds => null; - - public object GetResults(JToken output) => null; -} diff --git a/src/Microsoft.Health.Dicom.Functions.Abstractions/ContentLengthBackFillhCheckpoint/ContentLengthCheckpointInput.cs b/src/Microsoft.Health.Dicom.Functions.Abstractions/ContentLengthBackFillhCheckpoint/ContentLengthCheckpointInput.cs deleted file mode 100644 index 5eeb59bcdb..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Abstractions/ContentLengthBackFillhCheckpoint/ContentLengthCheckpointInput.cs +++ /dev/null @@ -1,11 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Functions.ContentLengthBackFill; - -public class ContentLengthBackFillInput -{ - public BatchingOptions Batching { get; set; } -} diff --git a/src/Microsoft.Health.Dicom.Functions.Abstractions/DataCleanup/DataCleanupCheckPoint.cs b/src/Microsoft.Health.Dicom.Functions.Abstractions/DataCleanup/DataCleanupCheckPoint.cs deleted file mode 100644 index 4acbb2ee0f..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Abstractions/DataCleanup/DataCleanupCheckPoint.cs +++ /dev/null @@ -1,35 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Operations.Functions.DurableTask; -using Newtonsoft.Json.Linq; - -namespace Microsoft.Health.Dicom.Functions.DataCleanup; - -public class DataCleanupCheckPoint : DataCleanupInput, IOrchestrationCheckpoint -{ - public WatermarkRange? Completed { get; set; } - - public DateTime? CreatedTime { get; set; } - - public int? PercentComplete - { - get - { - if (Completed.HasValue) - { - WatermarkRange range = Completed.GetValueOrDefault(); - return range.End == 1 ? 100 : (int)((double)(range.End - range.Start + 1) / range.End * 100); - } - return 0; - } - } - public IReadOnlyCollection ResourceIds => null; - - public object GetResults(JToken output) => null; -} diff --git a/src/Microsoft.Health.Dicom.Functions.Abstractions/DataCleanup/DataCleanupInput.cs b/src/Microsoft.Health.Dicom.Functions.Abstractions/DataCleanup/DataCleanupInput.cs deleted file mode 100644 index 44d8f5cfdf..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Abstractions/DataCleanup/DataCleanupInput.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; - -namespace Microsoft.Health.Dicom.Functions.DataCleanup; - -public class DataCleanupInput -{ - public BatchingOptions Batching { get; set; } - - public DateTimeOffset StartFilterTimeStamp { get; set; } - - public DateTimeOffset EndFilterTimeStamp { get; set; } -} diff --git a/src/Microsoft.Health.Dicom.Functions.Abstractions/Export/ExportCheckpoint.cs b/src/Microsoft.Health.Dicom.Functions.Abstractions/Export/ExportCheckpoint.cs deleted file mode 100644 index 559791a64a..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Abstractions/Export/ExportCheckpoint.cs +++ /dev/null @@ -1,44 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using Microsoft.Health.Operations; -using Microsoft.Health.Operations.Functions.DurableTask; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace Microsoft.Health.Dicom.Functions.Export; - -/// -/// Represents a checkpoint for the export operation which includes metadata such as the progress. -/// -public class ExportCheckpoint : ExportInput, IOrchestrationCheckpoint -{ - /// - /// Gets or sets the optional progress made by the operation so far. - /// - /// The progress if any has been made so far; otherwise . - public ExportProgress Progress { get; set; } - - /// - public DateTime? CreatedTime { get; set; } - - /// - [JsonIgnore] - public int? PercentComplete => null; - - /// - [JsonIgnore] - public IReadOnlyCollection ResourceIds => null; - - /// - /// Gets the for the orchestration. - /// - /// The unused orchestration output. - /// The current state of the orchestration. - public object GetResults(JToken output) - => new ExportResults(Progress, ErrorHref); -} diff --git a/src/Microsoft.Health.Dicom.Functions.Abstractions/Export/ExportInput.cs b/src/Microsoft.Health.Dicom.Functions.Abstractions/Export/ExportInput.cs deleted file mode 100644 index 00e1da3697..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Abstractions/Export/ExportInput.cs +++ /dev/null @@ -1,46 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Core.Models.Export; - -namespace Microsoft.Health.Dicom.Functions.Export; - -/// -/// Represents input to the export operation. -/// -public class ExportInput -{ - /// - /// Gets or sets the source of the export operation. - /// - /// The options describing the source. - public ExportDataOptions Source { get; set; } - - /// - /// Gets or sets the destination of the export operation. - /// - /// The options describing the destination. - public ExportDataOptions Destination { get; set; } - - /// - /// Gets or sets the settings that dictate how the operation should be parallelized. - /// - /// A set of settings related to batching DICOM files for export. - public BatchingOptions Batching { get; set; } - - /// - /// Gets or sets the DICOM data partition from which the data is read. - /// - /// A DICOM partition entry. - public Partition Partition { get; set; } - - /// - /// Gets or sets the URI for containing the errors for this operation, if any. - /// - /// The for the resource containg export errors. - public Uri ErrorHref { get; set; } -} diff --git a/src/Microsoft.Health.Dicom.Functions.Abstractions/Export/ExportProgress.cs b/src/Microsoft.Health.Dicom.Functions.Abstractions/Export/ExportProgress.cs deleted file mode 100644 index 8736c56646..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Abstractions/Export/ExportProgress.cs +++ /dev/null @@ -1,124 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Text.Json.Serialization; -using EnsureThat; - -namespace Microsoft.Health.Dicom.Functions.Export; - -/// -/// Represents the progress made so far much an export operation. -/// -public readonly struct ExportProgress : IEquatable -{ - /// - /// Gets the number of DICOM files that have successfully been exported so far. - /// - /// The non-negative number of exported DICOM files. - public long Exported { get; } - - /// - /// Gets the number of DICOM resources that have failed to be exported so far. - /// - /// The non-negative number of DICOM resources that failed to be exported. - public long Skipped { get; } - - /// - /// Initializes a new instance of the structure based on the specified number of - /// DICOM files processed by an export operation. - /// - /// The number of files that were successfully exported. - /// The number of files that failed to be exported. - /// - /// is less than 0. - /// -or- - /// is less than 0. - /// - [JsonConstructor] - public ExportProgress(long exported, long skipped) - { - Exported = EnsureArg.IsGte(exported, 0, nameof(exported)); - Skipped = EnsureArg.IsGte(skipped, 0, nameof(skipped)); - } - - /// - /// Returns a new that adds the value of the specified - /// to the value of this instance. - /// - /// Another instance of the structure. - /// - /// An object whose values are the sums of the and properties - /// represented by this instance and . - /// - public ExportProgress Add(ExportProgress other) - => new ExportProgress(Exported + other.Exported, Skipped + other.Skipped); - - /// - /// Returns a value indicating whether this instance is equal to a specified object. - /// - /// The object to compare to this instance. - /// - /// if is an instance of and - /// equals the value of this instance; otherwise, . - /// - public override bool Equals(object obj) - => obj is ExportProgress other && Equals(other); - - /// - /// Returns a value indicating whether the value of this instance is equal to the value of the - /// specified instance. - /// - /// The object to compare to this instance. - /// - /// if the parameter equals the - /// value of this instance; otherwise, . - /// - public bool Equals(ExportProgress other) - => Exported == other.Exported && Skipped == other.Skipped; - - /// - /// Returns the hash code for this instance. - /// - /// A 32-bit signed integer hash code. - public override int GetHashCode() - => HashCode.Combine(Exported, Skipped); - - /// - /// Returns a new that adds the value of the specified values. - /// - /// An instance of the structure. - /// Another instance of the structure. - /// - /// An object whose values are the sums of the and properties - /// represented by the two parameters. - /// - public static ExportProgress operator +(ExportProgress x, ExportProgress y) - => x.Add(y); - - /// - /// Determines whether two specified instances of are equal. - /// - /// The first object to compare. - /// The second object to compare. - /// - /// if and - /// represent the same ; otherwise, . - /// - public static bool operator ==(ExportProgress left, ExportProgress right) - => left.Equals(right); - - /// - /// Determines whether two specified instances of are not equal. - /// - /// The first object to compare. - /// The second object to compare. - /// - /// if and - /// do not represent the same ; otherwise, . - /// - public static bool operator !=(ExportProgress left, ExportProgress right) - => !left.Equals(right); -} diff --git a/src/Microsoft.Health.Dicom.Functions.Abstractions/Export/ExportResults.cs b/src/Microsoft.Health.Dicom.Functions.Abstractions/Export/ExportResults.cs deleted file mode 100644 index 25f858f650..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Abstractions/Export/ExportResults.cs +++ /dev/null @@ -1,44 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; - -namespace Microsoft.Health.Dicom.Functions.Export; - -/// -/// Represents the current state of the export operation to end-users. -/// -public sealed class ExportResults -{ - /// - /// Gets the number of DICOM files that were successfully exported. - /// - /// The non-negative number of exported DICOM files. - public long Exported { get; } - - /// - /// Gets the number of DICOM resources that were skipped because they failed to be exported. - /// - /// The non-negative number of DICOM resources that failed to be exported. - public long Skipped { get; } - - /// - /// Gets the URI for containing the errors for this operation, if any. - /// - /// The for the resource containg export errors. - public Uri ErrorHref { get; } - - /// - /// Initializes a new instance of the structure based given progress. - /// - /// The progress made by the export operation so far. - /// The URI for the error log. - public ExportResults(ExportProgress progress, Uri errorHref) - { - Exported = progress.Exported; - Skipped = progress.Skipped; - ErrorHref = errorHref; - } -} diff --git a/src/Microsoft.Health.Dicom.Functions.Abstractions/Indexing/ReindexCheckpoint.cs b/src/Microsoft.Health.Dicom.Functions.Abstractions/Indexing/ReindexCheckpoint.cs deleted file mode 100644 index b1d58adcdf..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Abstractions/Indexing/ReindexCheckpoint.cs +++ /dev/null @@ -1,55 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Operations; -using Microsoft.Health.Operations.Functions.DurableTask; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace Microsoft.Health.Dicom.Functions.Indexing; - -/// -/// Represents the state of the reindexing orchestration that is serialized in the input. -/// -public sealed class ReindexCheckpoint : ReindexInput, IOrchestrationCheckpoint -{ - /// - /// Gets or sets the range of DICOM SOP instance watermarks that have been reindexed, if started. - /// - /// A range of instance watermarks if started; otherwise, . - public WatermarkRange? Completed { get; set; } - - /// - public DateTime? CreatedTime { get; set; } - - /// - [JsonIgnore] - public int? PercentComplete - { - get - { - if (Completed.HasValue) - { - WatermarkRange range = Completed.GetValueOrDefault(); - return range.End == 1 ? 100 : (int)((double)(range.End - range.Start + 1) / range.End * 100); - } - - return 0; - } - } - - /// - [JsonIgnore] - public IReadOnlyCollection ResourceIds => QueryTagKeys?.Select(x => x.ToString(CultureInfo.InvariantCulture)).ToList(); - - /// - public object GetResults(JToken output) - => null; // TODO: Expose metrics to users via the operation API? -} diff --git a/src/Microsoft.Health.Dicom.Functions.Abstractions/Indexing/ReindexInput.cs b/src/Microsoft.Health.Dicom.Functions.Abstractions/Indexing/ReindexInput.cs deleted file mode 100644 index 4c5c1caa1c..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Abstractions/Indexing/ReindexInput.cs +++ /dev/null @@ -1,26 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; - -namespace Microsoft.Health.Dicom.Functions.Indexing; - -/// -/// Represents the input to the reindexing operation. -/// -public class ReindexInput -{ - /// - /// Gets or sets the collection of newly added extended query tag keys to be reindexed. - /// - /// A collection of one or more keys for extended query tags in the store. - public IReadOnlyCollection QueryTagKeys { get; set; } - - /// - /// Gets or sets the options that configure how the operation batches groups of DICOM SOP instances for reindexing. - /// - /// A set of reindexing batching options. - public BatchingOptions Batching { get; set; } -} diff --git a/src/Microsoft.Health.Dicom.Functions.Abstractions/Microsoft.Health.Dicom.Functions.Abstractions.csproj b/src/Microsoft.Health.Dicom.Functions.Abstractions/Microsoft.Health.Dicom.Functions.Abstractions.csproj deleted file mode 100644 index b80ac3b7b0..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Abstractions/Microsoft.Health.Dicom.Functions.Abstractions.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - Common abstractions used by DICOM functions. - Microsoft.Health.Dicom.Functions - $(LibraryFrameworks) - - - - - - - - - - - - - - - diff --git a/src/Microsoft.Health.Dicom.Functions.Abstractions/Properties/AssemblyInfo.cs b/src/Microsoft.Health.Dicom.Functions.Abstractions/Properties/AssemblyInfo.cs deleted file mode 100644 index 2e456f0a71..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Abstractions/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,10 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Resources; - -[assembly: NeutralResourcesLanguage("en-us")] -[assembly: CLSCompliant(false)] diff --git a/src/Microsoft.Health.Dicom.Functions.Abstractions/Update/UpdateCheckpoint.cs b/src/Microsoft.Health.Dicom.Functions.Abstractions/Update/UpdateCheckpoint.cs deleted file mode 100644 index 587cb617b9..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Abstractions/Update/UpdateCheckpoint.cs +++ /dev/null @@ -1,53 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using Microsoft.Health.Operations; -using Microsoft.Health.Operations.Functions.DurableTask; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace Microsoft.Health.Dicom.Functions.Update; - -/// -/// Represents the state of the update orchestration that is serialized in the input. -/// -public sealed class UpdateCheckpoint : UpdateInput, IOrchestrationCheckpoint -{ - public int NumberOfStudyProcessed { get; set; } - - public int NumberOfStudyCompleted { get; set; } - - public int NumberOfStudyFailed { get; set; } - - public int TotalNumberOfStudies => StudyInstanceUids?.Count ?? 0; - - public int TotalNumberOfInstanceUpdated { get; set; } - - public IReadOnlyList Errors { get; set; } - - /// - public DateTime? CreatedTime { get; set; } - - /// - [JsonIgnore] - public int? PercentComplete - { - get - { - if (NumberOfStudyProcessed > 0) - { - return NumberOfStudyProcessed == TotalNumberOfStudies ? 100 : (int)(((double)(NumberOfStudyProcessed) / TotalNumberOfStudies) * 100); - } - - return 0; - } - } - - public IReadOnlyCollection ResourceIds => null; - - public object GetResults(JToken output) => new UpdateResult(NumberOfStudyProcessed, NumberOfStudyCompleted, TotalNumberOfInstanceUpdated, NumberOfStudyFailed, Errors); -} diff --git a/src/Microsoft.Health.Dicom.Functions.Abstractions/Update/UpdateInput.cs b/src/Microsoft.Health.Dicom.Functions.Abstractions/Update/UpdateInput.cs deleted file mode 100644 index 967d8006f2..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Abstractions/Update/UpdateInput.cs +++ /dev/null @@ -1,20 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using Microsoft.Health.Dicom.Core.Features.Partitioning; - -namespace Microsoft.Health.Dicom.Functions.Update; - -public class UpdateInput -{ - public Partition Partition { get; set; } - - public int PartitionKey { get; set; } - - public IReadOnlyList StudyInstanceUids { get; set; } - - public string ChangeDataset { get; set; } -} diff --git a/src/Microsoft.Health.Dicom.Functions.Abstractions/Update/UpdateResult.cs b/src/Microsoft.Health.Dicom.Functions.Abstractions/Update/UpdateResult.cs deleted file mode 100644 index 1c1c0e6be6..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Abstractions/Update/UpdateResult.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; - -namespace Microsoft.Health.Dicom.Functions.Update; - -public class UpdateResult -{ - public int StudyProcessed { get; } - - public int StudyUpdated { get; } - - public int StudyFailed { get; } - - public long InstanceUpdated { get; } - - public IReadOnlyList Errors { get; } - - public UpdateResult(int studyProcessed, int studyUpdated, long instanceUpdated, int studyFailed, IReadOnlyList errors) - { - StudyProcessed = studyProcessed; - StudyUpdated = studyUpdated; - InstanceUpdated = instanceUpdated; - StudyFailed = studyFailed; - Errors = errors; - } -} diff --git a/src/Microsoft.Health.Dicom.Functions.App/Directory.Build.props b/src/Microsoft.Health.Dicom.Functions.App/Directory.Build.props deleted file mode 100644 index dfd06d9f3b..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.App/Directory.Build.props +++ /dev/null @@ -1,10 +0,0 @@ - - - - - net6.0 - - - - - diff --git a/src/Microsoft.Health.Dicom.Functions.App/Directory.Build.targets b/src/Microsoft.Health.Dicom.Functions.App/Directory.Build.targets deleted file mode 100644 index 3d31a9c46f..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.App/Directory.Build.targets +++ /dev/null @@ -1,14 +0,0 @@ - - - - - true - - - diff --git a/src/Microsoft.Health.Dicom.Functions.App/Docker/Dockerfile b/src/Microsoft.Health.Dicom.Functions.App/Docker/Dockerfile deleted file mode 100644 index d5973a556e..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.App/Docker/Dockerfile +++ /dev/null @@ -1,36 +0,0 @@ -FROM mcr.microsoft.com/azure-functions/dotnet:4@sha256:ff9ffed072a45f958fc3ac0170180dc9520586f25cae7f041d093f5fc6c46f4b AS az-func-runtime -ENV ASPNETCORE_URLS=http://+:8080 \ - AzureWebJobsScriptRoot=/home/site/wwwroot \ - LANG=en_US.UTF-8 \ - LC_ALL=en_US.UTF-8 -RUN groupadd nonroot && \ - useradd -r -M -s /sbin/nologin -g nonroot -c nonroot nonroot && \ - chown -R nonroot:nonroot /azure-functions-host -USER nonroot -EXPOSE 8080 - -# Copy the DICOM Server repository and build the Azure Functions project -FROM mcr.microsoft.com/dotnet/sdk:8.0.301-alpine3.18-amd64@sha256:5ccd7acc1ff31f2a0377bcbc50bd0553c28d65cd4f5ec4366e68966aea60bf2f AS build -ARG BUILD_CONFIGURATION=Release -ARG CONTINUOUS_INTEGRATION_BUILD=false - -# Azure Functions v4 targets .NET 6 -RUN set -x && \ - apk update && \ - apk add --no-cache bash && \ - curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin -Channel 6.0 -InstallDir /usr/share/dotnet - -WORKDIR /dicom-server -COPY . . -WORKDIR /dicom-server/src/Microsoft.Health.Dicom.Functions.App -RUN dotnet build "Microsoft.Health.Dicom.Functions.App.csproj" -c $BUILD_CONFIGURATION -p:ContinuousIntegrationBuild=$CONTINUOUS_INTEGRATION_BUILD -warnaserror - -# Publish the Azure Functions from the build -FROM build as publish -ARG BUILD_CONFIGURATION=Release -RUN dotnet publish "Microsoft.Health.Dicom.Functions.App.csproj" -c $BUILD_CONFIGURATION --no-build -o /home/site/wwwroot - -# Copy the published application -FROM az-func-runtime AS dicom-az-func -WORKDIR /home/site/wwwroot -COPY --from=publish /home/site/wwwroot . diff --git a/src/Microsoft.Health.Dicom.Functions.App/Microsoft.Health.Dicom.Functions.App.csproj b/src/Microsoft.Health.Dicom.Functions.App/Microsoft.Health.Dicom.Functions.App.csproj deleted file mode 100644 index 8fa12f72da..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.App/Microsoft.Health.Dicom.Functions.App.csproj +++ /dev/null @@ -1,62 +0,0 @@ - - - - v4 - An example Azure Function app that supports Microsoft's Medical Imaging Server for DICOM. - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - PreserveNewest - - - PreserveNewest - Never - - - - diff --git a/src/Microsoft.Health.Dicom.Functions.App/Properties/AssemblyInfo.cs b/src/Microsoft.Health.Dicom.Functions.App/Properties/AssemblyInfo.cs deleted file mode 100644 index db408ff935..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.App/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,10 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Resources; - -[assembly: NeutralResourcesLanguage("en-us")] -[assembly: CLSCompliant(false)] diff --git a/src/Microsoft.Health.Dicom.Functions.App/Properties/serviceDependencies.json b/src/Microsoft.Health.Dicom.Functions.App/Properties/serviceDependencies.json deleted file mode 100644 index df4dcc9d88..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.App/Properties/serviceDependencies.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "dependencies": { - "appInsights1": { - "type": "appInsights" - }, - "storage1": { - "type": "storage", - "connectionId": "AzureWebJobsStorage" - } - } -} \ No newline at end of file diff --git a/src/Microsoft.Health.Dicom.Functions.App/Properties/serviceDependencies.local.json b/src/Microsoft.Health.Dicom.Functions.App/Properties/serviceDependencies.local.json deleted file mode 100644 index b804a28939..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.App/Properties/serviceDependencies.local.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "dependencies": { - "appInsights1": { - "type": "appInsights.sdk" - }, - "storage1": { - "type": "storage.emulator", - "connectionId": "AzureWebJobsStorage" - } - } -} \ No newline at end of file diff --git a/src/Microsoft.Health.Dicom.Functions.App/Startup.cs b/src/Microsoft.Health.Dicom.Functions.App/Startup.cs deleted file mode 100644 index 4bc7d2d2b1..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.App/Startup.cs +++ /dev/null @@ -1,29 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using Microsoft.Azure.Functions.Extensions.DependencyInjection; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Health.Dicom.Functions.Registration; -using Microsoft.Health.Operations.Functions; - -[assembly: FunctionsStartup(typeof(Microsoft.Health.Dicom.Functions.App.Startup))] -namespace Microsoft.Health.Dicom.Functions.App; - -public class Startup : FunctionsStartup -{ - public override void Configure(IFunctionsHostBuilder builder) - { - EnsureArg.IsNotNull(builder, nameof(builder)); - - IConfiguration config = builder.GetHostConfiguration(); - builder.Services - .ConfigureFunctions(config) - .AddBlobStorage(config) - .AddSqlServer(config) - .AddKeyVaultClient(config); - } -} diff --git a/src/Microsoft.Health.Dicom.Functions.App/host.json b/src/Microsoft.Health.Dicom.Functions.App/host.json deleted file mode 100644 index 7a944f785c..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.App/host.json +++ /dev/null @@ -1,124 +0,0 @@ -{ - "version": "2.0", - "BlobStore": { - "ConnectionString": null, - "Containers": { - "Metadata": "metadatacontainer", - "File": "dicomwebcontainer" - }, - "Initialization": { - "RetryDelay": "00:00:15", - "Timeout": "00:06:00" - }, - "Operations": { - "Download": { - "MaximumConcurrency": 5 - }, - "Upload": { - "MaximumConcurrency": 5 - } - }, - "Retry": { - "Delay": "00:00:04", - "MaxRetries": 6, - "Mode": "Exponential", - "NetworkTimeout": "00:02:00" - } - }, - "DicomFunctions": { - "DataCleanup": { - "MaxParallelThreads": 5, - "RetryOptions": { - "BackoffCoefficient": 3, - "FirstRetryInterval": "00:01:00", - "MaxNumberOfAttempts": 4 - } - }, - "ContentLengthBackFill": { - "MaxParallelThreads": 2, - "RetryOptions": { - "BackoffCoefficient": 3, - "FirstRetryInterval": "00:01:00", - "MaxNumberOfAttempts": 4 - } - }, - "Export": { - "BatchSize": 100, - "MaxParallelThreads": 5, - "MaxParallelBatches": 10, - "RetryOptions": { - "BackoffCoefficient": 3, - "FirstRetryInterval": "00:01:00", - "MaxNumberOfAttempts": 4 - }, - "Sinks": { - "AzureBlob": { - "AllowPublicAccess": true, - "AllowSasTokens": true - } - } - }, - "Indexing": { - "BatchSize": 100, - "MaxParallelThreads": 5, - "MaxParallelBatches": 10, - "RetryOptions": { - "BackoffCoefficient": 3, - "FirstRetryInterval": "00:01:00", - "MaxNumberOfAttempts": 4 - } - }, - "Update": { - "MaxParallelThreads": 5, - "RetryOptions": { - "BackoffCoefficient": 3, - "FirstRetryInterval": "00:01:00", - "MaxNumberOfAttempts": 4 - } - }, - "IndexMetricsCollection": { - "Frequency": "0 0 * * *" - } - }, - "Extensions": { - "DurableTask": { - "HubName": "DicomTaskHub" - } - }, - "KeyVault": { - "Enabled": false - }, - "Logging": { - "IncludeScopes": false, - "LogLevel": { - "Azure.Core": "Warning", - "Default": "Information", - "DurableTask": "Warning" - } - }, - "PurgeHistory": { - "Frequency": "0 0 * * *", - "MinimumAgeDays": 7, - "Statuses": [ "Completed" ], - "ExcludeFunctions": [ ] - }, - "SqlServer": { - "Retry": { - "Mode": "Exponential", - "Settings ": { - "NumberOfTries": 5, - "DeltaTime": "00:00:01", - "MaxTimeInterval": "00:00:20" - } - } - }, - "ExternalBlobStore": { - "BlobContainerUri": null, - "StorageDirectory": "DICOM/" - }, - "DicomServer": { - "Features": { - "EnableExternalStore": false - } - } -} diff --git a/src/Microsoft.Health.Dicom.Functions.App/local.settings.json b/src/Microsoft.Health.Dicom.Functions.App/local.settings.json deleted file mode 100644 index 9d909c57f7..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.App/local.settings.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "IsEncrypted": false, - "Values": { - "AzureFunctionsJobHost__DicomFunctions__Indexing__MaxParallelBatches": "1", - "AzureFunctionsJobHost__DicomFunctions__IndexMetricsCollection__Frequency": "0 0 * * *", - "AzureFunctionsJobHost__Logging__Console__IsEnabled": "true", - "AzureFunctionsJobHost__SqlServer__ConnectionString": "server=(local);Initial Catalog=Dicom;Integrated Security=true;TrustServerCertificate=true", - "AzureFunctionsJobHost__BlobStore__ConnectionString": "UseDevelopmentStorage=true", - "AzureFunctionsJobHost__ExternalBlobStore__ConnectionString": "UseDevelopmentStorage=true", - "AzureFunctionsJobHost__ExternalBlobStore__ContainerName": "dicomexternalcontainer", - "AzureWebJobsStorage": "UseDevelopmentStorage=true", - "FUNCTIONS_WORKER_RUNTIME": "dotnet", - "AzureFunctionsJobHost__DicomFunctions__Update__RetryOptions__MaxNumberOfAttempts": "1" - } -} diff --git a/src/Microsoft.Health.Dicom.Functions.Client.UnitTests/DicomAzureFunctionsClientTests.cs b/src/Microsoft.Health.Dicom.Functions.Client.UnitTests/DicomAzureFunctionsClientTests.cs deleted file mode 100644 index 86af67694a..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Client.UnitTests/DicomAzureFunctionsClientTests.cs +++ /dev/null @@ -1,535 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Linq.Expressions; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using FellowOakDicom; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Microsoft.Azure.WebJobs.Extensions.DurableTask.ContextImplementations; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Features.Operations; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Core.Features.Routing; -using Microsoft.Health.Dicom.Core.Models.Export; -using Microsoft.Health.Dicom.Core.Models.Operations; -using Microsoft.Health.Dicom.Core.Models.Update; -using Microsoft.Health.Dicom.Core.Serialization; -using Microsoft.Health.Dicom.Functions.Export; -using Microsoft.Health.Dicom.Functions.Indexing; -using Microsoft.Health.Dicom.Functions.Update; -using Microsoft.Health.FellowOakDicom.Serialization; -using Microsoft.Health.Operations; -using Newtonsoft.Json.Linq; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Functions.Client.UnitTests; - -public class DicomAzureFunctionsClientTests -{ - private readonly IDurableClient _durableClient; - private readonly IUrlResolver _urlResolver; - private readonly IDicomOperationsResourceStore _resourceStore; - private readonly DicomFunctionOptions _options; - private readonly DicomAzureFunctionsClient _client; - - public DicomAzureFunctionsClientTests() - { - IDurableClientFactory durableClientFactory = Substitute.For(); - _durableClient = Substitute.For(); - durableClientFactory.CreateClient().Returns(_durableClient); - - _urlResolver = Substitute.For(); - _resourceStore = Substitute.For(); - _options = new DicomFunctionOptions - { - Export = new FanOutFunctionOptions - { - Name = FunctionNames.ExportDicomFiles, - Batching = new BatchingOptions - { - MaxParallelCount = 2, - Size = 50, - }, - }, - Indexing = new FanOutFunctionOptions - { - Name = FunctionNames.ReindexInstances, - Batching = new BatchingOptions - { - MaxParallelCount = 1, - Size = 100, - }, - }, - Update = new FanOutFunctionOptions - { - Name = FunctionNames.UpdateInstances, - Batching = new BatchingOptions - { - MaxParallelCount = 1, - Size = 100, - }, - }, - }; - - var jsonOptions = new JsonSerializerOptions(); - jsonOptions.Converters.Add(new DicomJsonConverter(writeTagsAsKeywords: true, autoValidate: false, numberSerializationMode: NumberSerializationMode.PreferablyAsNumber)); - jsonOptions.Converters.Add(new ExportDataOptionsJsonConverter()); - _client = new DicomAzureFunctionsClient( - durableClientFactory, - _urlResolver, - _resourceStore, - Options.Create(_options), - Options.Create(jsonOptions), - NullLogger.Instance); - } - - [Fact] - public void GivenNullArguments_WhenConstructing_ThenThrowArgumentNullException() - { - IDurableClientFactory durableClientFactory = Substitute.For(); - IExtendedQueryTagStore extendedQueryTagStore = Substitute.For(); - IUrlResolver urlResolver = Substitute.For(); - IDicomOperationsResourceStore resourceStore = Substitute.For(); - var options = Options.Create(new DicomFunctionOptions()); - var jsonOptions = Options.Create(new JsonSerializerOptions()); - - Assert.Throws( - () => new DicomAzureFunctionsClient(null, urlResolver, resourceStore, options, jsonOptions, NullLogger.Instance)); - - Assert.Throws( - () => new DicomAzureFunctionsClient(durableClientFactory, null, resourceStore, options, jsonOptions, NullLogger.Instance)); - - Assert.Throws( - () => new DicomAzureFunctionsClient(durableClientFactory, urlResolver, null, options, jsonOptions, NullLogger.Instance)); - - Assert.Throws( - () => new DicomAzureFunctionsClient(durableClientFactory, urlResolver, resourceStore, null, jsonOptions, NullLogger.Instance)); - - Assert.Throws( - () => new DicomAzureFunctionsClient(durableClientFactory, urlResolver, resourceStore, options, jsonOptions, null)); - } - - [Fact] - public async Task GivenNotFound_WhenGettingState_ThenReturnNull() - { - string instanceId = OperationId.Generate(); - using var source = new CancellationTokenSource(); - - _durableClient.GetStatusAsync(instanceId, showInput: true).Returns(Task.FromResult(null)); - - Assert.Null(await _client.GetStateAsync(Guid.Parse(instanceId), source.Token)); - - await _durableClient.Received(1).GetStatusAsync(instanceId, showInput: true); - _urlResolver.DidNotReceiveWithAnyArgs().ResolveQueryTagUri(default); - } - - [Fact] - public async Task GivenUnknownName_WhenGettingState_ThenReturnNull() - { - string instanceId = OperationId.Generate(); - using var source = new CancellationTokenSource(); - - _durableClient - .GetStatusAsync(instanceId, showInput: true) - .Returns(new DurableOrchestrationStatus - { - InstanceId = instanceId, - Name = "Foobar", - RuntimeStatus = OrchestrationRuntimeStatus.Running, - }); - - Assert.Null(await _client.GetStateAsync(Guid.Parse(instanceId), source.Token)); - - await _durableClient.Received(1).GetStatusAsync(instanceId, showInput: true); - _urlResolver.DidNotReceiveWithAnyArgs().ResolveQueryTagUri(default); - } - - [Theory] - [InlineData(false, false)] - [InlineData(true, false)] - [InlineData(true, true)] - public async Task GivenReindexOperation_WhenGettingState_ThenReturnStatus(bool populateInput, bool overrideCreatedTime) - { - string instanceId = OperationId.Generate(); - Guid operationId = Guid.Parse(instanceId); - var createdTime = new DateTime(2021, 06, 08, 1, 2, 3, DateTimeKind.Utc); - var tagKeys = new int[] { 1, 2, 3 }; - var tagPaths = tagKeys.Select(x => string.Join("", Enumerable.Repeat(x.ToString("D2", CultureInfo.InvariantCulture), 4))).ToArray(); - var expectedResourceUrls = tagPaths.Select(x => new Uri($"https://dicom-unit-tests/extendedquerytags/{x}", UriKind.Absolute)).ToArray(); - - using var source = new CancellationTokenSource(); - - _durableClient - .GetStatusAsync(instanceId, showInput: true) - .Returns(new DurableOrchestrationStatus - { - CreatedTime = createdTime, - CustomStatus = null, - History = null, - Input = populateInput - ? JObject.FromObject( - new ReindexCheckpoint - { - Completed = new WatermarkRange(21, 100), - CreatedTime = overrideCreatedTime ? createdTime.AddHours(-1) : null, - QueryTagKeys = tagKeys, - }) - : null, - InstanceId = instanceId, - LastUpdatedTime = createdTime.AddMinutes(15), - Name = FunctionNames.ReindexInstances, - Output = null, - RuntimeStatus = OrchestrationRuntimeStatus.Running, - }); - - if (populateInput) - { - _resourceStore - .ResolveQueryTagKeysAsync( - Arg.Is>(x => x.SequenceEqual(tagKeys)), - source.Token) - .Returns(tagPaths.ToAsyncEnumerable()); - } - - for (int i = 0; i < tagPaths.Length; i++) - { - _urlResolver.ResolveQueryTagUri(tagPaths[i]).Returns(expectedResourceUrls[i]); - } - - IOperationState actual = await _client.GetStateAsync(operationId, source.Token); - Assert.NotNull(actual); - Assert.Equal(overrideCreatedTime ? createdTime.AddHours(-1) : createdTime, actual.CreatedTime); - Assert.Equal(createdTime.AddMinutes(15), actual.LastUpdatedTime); - Assert.Equal(operationId, actual.OperationId); - Assert.Equal(populateInput ? 80 : 0, actual.PercentComplete); - Assert.True(actual.Resources.SequenceEqual(populateInput ? expectedResourceUrls : Array.Empty())); - Assert.Null(actual.Results); - Assert.Equal(OperationStatus.Running, actual.Status); - Assert.Equal(DicomOperation.Reindex, actual.Type); - - await _durableClient.Received(1).GetStatusAsync(instanceId, showInput: true); - - if (populateInput) - { - _resourceStore - .Received(1) - .ResolveQueryTagKeysAsync( - Arg.Is>(x => x.SequenceEqual(tagKeys)), - source.Token); - - foreach (string path in tagPaths) - { - _urlResolver.Received(1).ResolveQueryTagUri(path); - } - } - } - - [Fact] - public async Task GivenExportOperation_WhenGettingState_ThenSuccessfullyParseCheckpointWithResults() - { - string instanceId = OperationId.Generate(); - Guid operationId = Guid.Parse(instanceId); - var createdTime = new DateTime(2022, 08, 26, 1, 2, 3, DateTimeKind.Utc); - - using var source = new CancellationTokenSource(); - - _durableClient - .GetStatusAsync(instanceId, showInput: true) - .Returns(new DurableOrchestrationStatus - { - CreatedTime = createdTime, - CustomStatus = null, - History = null, - Input = JObject.FromObject( - new ExportCheckpoint - { - ErrorHref = new Uri($"https://unit-test.blob.core.windows.net/export/{instanceId}/errors.log"), - Progress = new ExportProgress(1000, 2), - }), - InstanceId = instanceId, - LastUpdatedTime = createdTime.AddMinutes(5), - Name = FunctionNames.ExportDicomFiles, - Output = null, - RuntimeStatus = OrchestrationRuntimeStatus.Running, - }); - - IOperationState actual = await _client.GetStateAsync(operationId, source.Token); - Assert.NotNull(actual); - Assert.Equal(createdTime, actual.CreatedTime); - Assert.Equal(createdTime.AddMinutes(5), actual.LastUpdatedTime); - Assert.Equal(operationId, actual.OperationId); - Assert.Null(actual.PercentComplete); - Assert.Null(actual.Resources); - Assert.Equal(OperationStatus.Running, actual.Status); - Assert.Equal(DicomOperation.Export, actual.Type); - - var results = actual.Results as ExportResults; - Assert.NotNull(results); - Assert.Equal(new Uri($"https://unit-test.blob.core.windows.net/export/{instanceId}/errors.log"), results.ErrorHref); - Assert.Equal(1000L, results.Exported); - Assert.Equal(2L, results.Skipped); - - await _durableClient.Received(1).GetStatusAsync(instanceId, showInput: true); - } - - [Fact] - public async Task GivenCriteria_WhenSearchingForOperations_ThenReturnPaginatedResults() - { - using var source = new CancellationTokenSource(); - - DateTime end = DateTime.UtcNow; - DateTime start = end.AddHours(-2); - - var expectedInstances = new List - { - new DurableOrchestrationStatus { InstanceId = Guid.NewGuid().ToString("N"), Name = "ReindexInstancesAsync" }, - new DurableOrchestrationStatus { InstanceId = "foo", Name = "ReindexInstancesAsync" }, - new DurableOrchestrationStatus { InstanceId = Guid.NewGuid().ToString("N"), Name = "bar" }, - new DurableOrchestrationStatus { InstanceId = Guid.NewGuid().ToString("N"), Name = "ExportDicomFilesAsync" }, - new DurableOrchestrationStatus { InstanceId = Guid.NewGuid().ToString("N"), Name = "ReindexInstancesAsync" }, - new DurableOrchestrationStatus { InstanceId = Guid.NewGuid().ToString("N"), Name = "ReindexInstancesAsync" }, - new DurableOrchestrationStatus { InstanceId = Guid.NewGuid().ToString("N"), Name = "ExportDicomFilesAsync" }, - }; - - var expected = new List - { - new OperationReference(Guid.Parse(expectedInstances[0].InstanceId), new Uri("https://dicom.unit.test/operations/" + expectedInstances[0].InstanceId)), - new OperationReference(Guid.Parse(expectedInstances[4].InstanceId), new Uri("https://dicom.unit.test/operations/" + expectedInstances[4].InstanceId)), - new OperationReference(Guid.Parse(expectedInstances[5].InstanceId), new Uri("https://dicom.unit.test/operations/" + expectedInstances[5].InstanceId)), - }; - - _durableClient - .ListInstancesAsync(Arg.Is(GetPredicate(null)), source.Token) - .Returns(new OrchestrationStatusQueryResult - { - ContinuationToken = "AAAA", - DurableOrchestrationState = expectedInstances.Take(3).ToList(), - }); - - _durableClient - .ListInstancesAsync(Arg.Is(GetPredicate("AAAA")), source.Token) - .Returns(new OrchestrationStatusQueryResult - { - ContinuationToken = "BBBB", - DurableOrchestrationState = expectedInstances.Skip(3).Take(2).ToList(), - }); - - _durableClient - .ListInstancesAsync(Arg.Is(GetPredicate("BBBB")), source.Token) - .Returns(new OrchestrationStatusQueryResult - { - ContinuationToken = null, - DurableOrchestrationState = expectedInstances.Skip(5).ToList(), - }); - - foreach (OperationReference operation in expected) - { - _urlResolver.ResolveOperationStatusUri(operation.Id).Returns(operation.Href); - } - - List actual = await _client - .FindOperationsAsync( - new OperationQueryCondition - { - CreatedTimeFrom = start, - CreatedTimeTo = end, - Operations = new DicomOperation[] { DicomOperation.Reindex }, - Statuses = new OperationStatus[] { OperationStatus.NotStarted, OperationStatus.Running }, - }, - source.Token) - .ToListAsync(); - - await _durableClient - .Received(1) - .ListInstancesAsync(Arg.Is(GetPredicate(null)), source.Token); - - await _durableClient - .Received(1) - .ListInstancesAsync(Arg.Is(GetPredicate("AAAA")), source.Token); - - await _durableClient - .Received(1) - .ListInstancesAsync(Arg.Is(GetPredicate("BBBB")), source.Token); - - foreach (OperationReference operation in expected) - { - _urlResolver.Received(1).ResolveOperationStatusUri(operation.Id); - } - - Assert.True(actual.Select(x => x.Id).SequenceEqual(expected.Select(x => x.Id))); - Assert.True(actual.Select(x => x.Href).SequenceEqual(expected.Select(x => x.Href))); - - Expression> GetPredicate(string continuationToken) - => (OrchestrationStatusQueryCondition x) => - x.ContinuationToken == continuationToken && - x.CreatedTimeFrom == start && - x.CreatedTimeTo == end && - x.InstanceIdPrefix == null && - x.ShowInput && - x.RuntimeStatus.SequenceEqual( - new OrchestrationRuntimeStatus[] - { - OrchestrationRuntimeStatus.Pending, - OrchestrationRuntimeStatus.Running, - OrchestrationRuntimeStatus.ContinuedAsNew - }) && - x.TaskHubNames == null; - } - - [Fact] - public async Task GivenNullTagKeys_WhenStartingReindex_ThenThrowArgumentNullException() - { - using var source = new CancellationTokenSource(); - - await Assert.ThrowsAsync(() => _client.StartReindexingInstancesAsync(Guid.NewGuid(), null, source.Token)); - - await _durableClient.DidNotReceiveWithAnyArgs().StartNewAsync(default, default); - } - - [Fact] - public async Task GivenNoTagKeys_WhenStartingReindex_ThenThrowArgumentException() - { - using var source = new CancellationTokenSource(); - - await Assert.ThrowsAsync(() => _client.StartReindexingInstancesAsync(Guid.NewGuid(), Array.Empty(), source.Token)); - - await _durableClient.DidNotReceiveWithAnyArgs().StartNewAsync(default, default); - } - - [Fact] - public async Task GivenAssignedKeys_WhenStartingReindex_ThenStartOrchestration() - { - string instanceId = OperationId.Generate(); - var operationId = Guid.Parse(instanceId); - var tagKeys = new int[] { 10, 42 }; - var uri = new Uri("http://my-operation/" + operationId); - - using var source = new CancellationTokenSource(); - - _durableClient - .StartNewAsync( - FunctionNames.ReindexInstances, - instanceId, - Arg.Is(x => x.QueryTagKeys.SequenceEqual(tagKeys))) - .Returns(instanceId); - _urlResolver.ResolveOperationStatusUri(operationId).Returns(uri); - - OperationReference actual = await _client.StartReindexingInstancesAsync(operationId, tagKeys, source.Token); - Assert.Equal(operationId, actual.Id); - Assert.Equal(uri, actual.Href); - - await _durableClient - .Received(1) - .StartNewAsync( - FunctionNames.ReindexInstances, - instanceId, - Arg.Is(x => x.QueryTagKeys.SequenceEqual(tagKeys))); - _urlResolver.Received(1).ResolveOperationStatusUri(operationId); - } - - [Fact] - public async Task GivenNullArgs_WhenStartingExport_ThenThrowArgumentNullException() - { - await Assert.ThrowsAsync(() => _client.StartExportAsync(Guid.NewGuid(), null, new Uri("https://errors.log"), Partition.Default)); - await Assert.ThrowsAsync(() => _client.StartExportAsync(Guid.NewGuid(), new ExportSpecification(), null, Partition.Default)); - await Assert.ThrowsAsync(() => _client.StartExportAsync(Guid.NewGuid(), new ExportSpecification(), new Uri("https://errors.log"), null)); - } - - [Fact] - public async Task GivenValidArgs_WhenStartingExport_ThenStartOrchestration() - { - var operationId = Guid.NewGuid(); - var spec = new ExportSpecification - { - Destination = new ExportDataOptions(ExportDestinationType.AzureBlob), - Source = new ExportDataOptions(ExportSourceType.Identifiers), - }; - var errorHref = new Uri($"https://test.blob.core.windows.net/export/{operationId:N}/errors.log"); - var partition = new Partition(17, "test"); - var url = new Uri("http://foo.com/bar/operations/" + operationId.ToString(OperationId.FormatSpecifier)); - - _durableClient - .StartNewAsync( - FunctionNames.ExportDicomFiles, - operationId.ToString(OperationId.FormatSpecifier), - Arg.Is(x => ReferenceEquals(_options.Export.Batching, x.Batching) - && ReferenceEquals(spec.Destination, x.Destination) - && ReferenceEquals(errorHref, x.ErrorHref) - && ReferenceEquals(partition, x.Partition) - && ReferenceEquals(spec.Source, x.Source))) - .Returns(operationId.ToString(OperationId.FormatSpecifier)); - _urlResolver - .ResolveOperationStatusUri(operationId) - .Returns(url); - - using var tokenSource = new CancellationTokenSource(); - OperationReference actual = await _client.StartExportAsync(operationId, spec, errorHref, partition, tokenSource.Token); - - await _durableClient - .Received(1) - .StartNewAsync( - FunctionNames.ExportDicomFiles, - operationId.ToString(OperationId.FormatSpecifier), - Arg.Is(x => ReferenceEquals(_options.Export.Batching, x.Batching) - && ReferenceEquals(spec.Destination, x.Destination) - && ReferenceEquals(errorHref, x.ErrorHref) - && ReferenceEquals(partition, x.Partition) - && ReferenceEquals(spec.Source, x.Source))); - _urlResolver - .Received(1) - .ResolveOperationStatusUri(operationId); - - Assert.Equal(operationId, actual.Id); - Assert.Equal(url, actual.Href); - } - - [Fact] - public async Task GivenValidArgs_WhenStartingUpdate_ThenStartOrchestration() - { - string instanceId = OperationId.Generate(); - var operationId = Guid.Parse(instanceId); - var studyUids = new string[] { "1", "2" }; - var ds = new DicomDataset - { - { DicomTag.PatientName, "Patient Name" } - }; - - var updateSpec = new UpdateSpecification(studyUids, ds); - - var uri = new Uri("http://my-operation/" + operationId); - - using var source = new CancellationTokenSource(); - - _durableClient - .StartNewAsync( - FunctionNames.UpdateInstances, - instanceId, - Arg.Is(x => x.StudyInstanceUids.SequenceEqual(studyUids))) - .Returns(instanceId); - _urlResolver.ResolveOperationStatusUri(operationId).Returns(uri); - - OperationReference actual = await _client.StartUpdateOperationAsync(operationId, updateSpec, Partition.Default, source.Token); - Assert.Equal(operationId, actual.Id); - Assert.Equal(uri, actual.Href); - - await _durableClient - .Received(1) - .StartNewAsync( - FunctionNames.UpdateInstances, - instanceId, - Arg.Is(x => x.StudyInstanceUids.SequenceEqual(studyUids))); - _urlResolver.Received(1).ResolveOperationStatusUri(operationId); - } -} diff --git a/src/Microsoft.Health.Dicom.Functions.Client.UnitTests/Extensions/AzureComponentFactoryExtensionsTests.cs b/src/Microsoft.Health.Dicom.Functions.Client.UnitTests/Extensions/AzureComponentFactoryExtensionsTests.cs deleted file mode 100644 index 4a1b47329e..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Client.UnitTests/Extensions/AzureComponentFactoryExtensionsTests.cs +++ /dev/null @@ -1,126 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using Azure.Data.Tables; -using Azure.Storage.Blobs; -using Azure.Storage.Queues; -using Microsoft.Extensions.Azure; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Health.Dicom.Functions.Client.Extensions; -using Xunit; - -namespace Microsoft.Health.Dicom.Functions.Client.UnitTests.Extensions; - -public class AzureComponentFactoryExtensionsTests -{ - private const string AccountName = "unittest"; - private const string SectionName = "AzureWebJobsStorage"; - - private readonly AzureComponentFactory _factory; - - public AzureComponentFactoryExtensionsTests() - { - ServiceCollection services = new ServiceCollection(); - - services - .AddLogging() - .AddAzureClientsCore(); - - _factory = services.BuildServiceProvider().GetRequiredService(); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void GivenBasicConfiguration_WhenCreatingBlobServiceClient_ThenCreateClient(bool direct) - { - IConfiguration config = CreateConnectionStringConfiguration(SectionName, direct); - BlobServiceClient actual = _factory.CreateBlobServiceClient(config.GetSection(SectionName)); - Assert.Equal(new Uri("http://127.0.0.1:10000/devstoreaccount1"), actual.Uri); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void GivenBasicConfiguration_WhenCreatingQueueServiceClient_ThenCreateClient(bool direct) - { - IConfiguration config = CreateConnectionStringConfiguration(SectionName, direct); - QueueServiceClient actual = _factory.CreateQueueServiceClient(config.GetSection(SectionName)); - Assert.Equal(new Uri("http://127.0.0.1:10001/devstoreaccount1"), actual.Uri); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void GivenBasicConfiguration_WhenCreatingTableServiceClient_ThenCreateClient(bool direct) - { - IConfiguration config = CreateConnectionStringConfiguration(SectionName, direct); - TableServiceClient actual = _factory.CreateTableServiceClient(config.GetSection(SectionName)); - Assert.Equal(new Uri("http://127.0.0.1:10002/devstoreaccount1"), actual.Uri); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void GivenManagedIdentityConfiguration_WhenCreatingBlobServiceClient_ThenCreateClient(bool specifyServiceUri) - { - IConfiguration config = CreateManagedIdentityConfiguration(SectionName, AccountName, specifyServiceUri ? "blob" : null); - BlobServiceClient actual = _factory.CreateBlobServiceClient(config.GetSection(SectionName)); - Assert.Equal(new Uri($"https://{AccountName}.blob.core.windows.net"), actual.Uri); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void GivenManagedIdentityConfiguration_WhenCreatingQueueServiceClient_ThenCreateClient(bool specifyServiceUri) - { - IConfiguration config = CreateManagedIdentityConfiguration(SectionName, AccountName, specifyServiceUri ? "queue" : null); - QueueServiceClient actual = _factory.CreateQueueServiceClient(config.GetSection(SectionName)); - Assert.Equal(new Uri($"https://{AccountName}.queue.core.windows.net"), actual.Uri); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void GivenManagedIdentityConfiguration_WhenCreatingTableServiceClient_ThenCreateClient(bool specifyServiceUri) - { - IConfiguration config = CreateManagedIdentityConfiguration(SectionName, AccountName, specifyServiceUri ? "table" : null); - TableServiceClient actual = _factory.CreateTableServiceClient(config.GetSection(SectionName)); - Assert.Equal(new Uri($"https://{AccountName}.table.core.windows.net"), actual.Uri); - } - - private static IConfiguration CreateConnectionStringConfiguration(string section, bool direct) - { - string key = direct ? section : section + ":ConnectionString"; - return new ConfigurationBuilder() - .AddInMemoryCollection( - new KeyValuePair[] - { - KeyValuePair.Create(key, "UseDevelopmentStorage=true"), - }) - .Build(); - } - - private static IConfiguration CreateManagedIdentityConfiguration(string section, string accountName, string service = null) - { - string prefix = section + ":"; - KeyValuePair connectionProperty = service is null - ? KeyValuePair.Create(prefix + "AccountName", accountName) - : KeyValuePair.Create(prefix + service + "ServiceUri", $"https://{accountName}.{service}.core.windows.net"); - - return new ConfigurationBuilder() - .AddInMemoryCollection( - new KeyValuePair[] - { - connectionProperty, - KeyValuePair.Create(prefix + "ClientId", Guid.NewGuid().ToString()), - KeyValuePair.Create(prefix + "Credential", "ManagedIdentity"), - }) - .Build(); - } -} diff --git a/src/Microsoft.Health.Dicom.Functions.Client.UnitTests/Extensions/DurableOrchestrationStatusExtensionsTests.cs b/src/Microsoft.Health.Dicom.Functions.Client.UnitTests/Extensions/DurableOrchestrationStatusExtensionsTests.cs deleted file mode 100644 index ee1d2789a0..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Client.UnitTests/Extensions/DurableOrchestrationStatusExtensionsTests.cs +++ /dev/null @@ -1,26 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Microsoft.Health.Dicom.Core.Models.Operations; -using Microsoft.Health.Dicom.Functions.Client.Extensions; -using Xunit; - -namespace Microsoft.Health.Dicom.Functions.Client.UnitTests.Extensions; - -public class DurableOrchestrationStatusExtensionsTests -{ - [Theory] - [InlineData(null, DicomOperation.Unknown)] - [InlineData("foo", DicomOperation.Unknown)] - [InlineData("Unknown", DicomOperation.Unknown)] - [InlineData(FunctionNames.ReindexInstances, DicomOperation.Reindex)] - [InlineData(FunctionNames.ExportDicomFiles, DicomOperation.Export)] - [InlineData("reindexINSTANCESasync", DicomOperation.Reindex)] - public void GivenOrchestrationStatus_WhenGettingDicomOperation_ThenConvertNameToType(string name, DicomOperation expected) - { - Assert.Equal(expected, new DurableOrchestrationStatus { Name = name }.GetDicomOperation()); - } -} diff --git a/src/Microsoft.Health.Dicom.Functions.Client.UnitTests/Extensions/OperationQueryConditionExtensionsTests.cs b/src/Microsoft.Health.Dicom.Functions.Client.UnitTests/Extensions/OperationQueryConditionExtensionsTests.cs deleted file mode 100644 index 6fa78513d8..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Client.UnitTests/Extensions/OperationQueryConditionExtensionsTests.cs +++ /dev/null @@ -1,41 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Linq; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Microsoft.Health.Dicom.Core.Models.Operations; -using Microsoft.Health.Dicom.Functions.Client.Extensions; -using Microsoft.Health.Operations; -using Xunit; - -namespace Microsoft.Health.Dicom.Functions.Client.UnitTests.Extensions; - -public class OperationQueryConditionExtensionsTests -{ - [Theory] - [InlineData(OperationStatus.Unknown)] - [InlineData((OperationStatus)42)] - public void GivenInvalidStatus_WhenGettingRuntimeStatus_ThenThrow(OperationStatus status) - { - var query = new OperationQueryCondition { Statuses = new OperationStatus[] { status } }; - Assert.Throws(() => query.ForDurableFunctions()); - } - - [Theory] - [InlineData(OperationStatus.Canceled, OrchestrationRuntimeStatus.Canceled, OrchestrationRuntimeStatus.Terminated)] -#pragma warning disable CS0618 - [InlineData(OperationStatus.Completed, OrchestrationRuntimeStatus.Completed)] -#pragma warning restore CS0618 - [InlineData(OperationStatus.Failed, OrchestrationRuntimeStatus.Failed)] - [InlineData(OperationStatus.NotStarted, OrchestrationRuntimeStatus.Pending)] - [InlineData(OperationStatus.Running, OrchestrationRuntimeStatus.Running, OrchestrationRuntimeStatus.ContinuedAsNew)] - [InlineData(OperationStatus.Succeeded, OrchestrationRuntimeStatus.Completed)] - public void GivenOperationStatus_WhenGettingRuntimeStatus_ThenMapCorrectly(OperationStatus status, params OrchestrationRuntimeStatus[] expected) - { - var query = new OperationQueryCondition { Statuses = new OperationStatus[] { status } }; - Assert.True(expected.SequenceEqual(query.ForDurableFunctions().RuntimeStatus)); - } -} diff --git a/src/Microsoft.Health.Dicom.Functions.Client.UnitTests/HealthChecks/DurableTaskHealthCheckTests.cs b/src/Microsoft.Health.Dicom.Functions.Client.UnitTests/HealthChecks/DurableTaskHealthCheckTests.cs deleted file mode 100644 index 3eb346c769..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Client.UnitTests/HealthChecks/DurableTaskHealthCheckTests.cs +++ /dev/null @@ -1,89 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Diagnostics.HealthChecks; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Health.Core.Features.Health; -using Microsoft.Health.Dicom.Functions.Client.HealthChecks; -using Microsoft.Health.Dicom.Functions.Client.TaskHub; -using Microsoft.Health.Encryption.Customer.Health; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Functions.Client.UnitTests.HealthChecks; - -public class DurableTaskHealthCheckTests -{ - private readonly ITaskHubClient _client = Substitute.For(); - private readonly ValueCache _customerKeyHealthCache = new ValueCache(); - private readonly DurableTaskHealthCheck _healthCheck; - - public DurableTaskHealthCheckTests() - { - _customerKeyHealthCache.Set(new CustomerKeyHealth { IsHealthy = true }); - _healthCheck = new DurableTaskHealthCheck(_client, _customerKeyHealthCache, NullLogger.Instance); - } - - [Fact] - public async Task GivenMissingTaskHub_WhenCheckingHealth_ThenReturnUnhealthy() - { - using var tokenSource = new CancellationTokenSource(); - - _client.GetTaskHubAsync(tokenSource.Token).Returns((ITaskHub)null); - - HealthCheckResult actual = await _healthCheck.CheckHealthAsync(new HealthCheckContext(), tokenSource.Token); - Assert.Equal(HealthStatus.Unhealthy, actual.Status); - - await _client.Received(1).GetTaskHubAsync(tokenSource.Token); - } - - [Fact] - public async Task GivenUnhealthyTaskHub_WhenCheckingHealth_ThenReturnUnhealthy() - { - using var tokenSource = new CancellationTokenSource(); - ITaskHub taskHub = Substitute.For(); - - _client.GetTaskHubAsync(tokenSource.Token).Returns(taskHub); - taskHub.IsHealthyAsync(tokenSource.Token).Returns(false); - - HealthCheckResult actual = await _healthCheck.CheckHealthAsync(new HealthCheckContext(), tokenSource.Token); - Assert.Equal(HealthStatus.Unhealthy, actual.Status); - - await _client.Received(1).GetTaskHubAsync(tokenSource.Token); - await taskHub.Received(1).IsHealthyAsync(tokenSource.Token); - } - - [Fact] - public async Task GivenAvailableHealthCheck_WhenCheckingHealth_ThenReturnHealthy() - { - using var tokenSource = new CancellationTokenSource(); - ITaskHub taskHub = Substitute.For(); - - _client.GetTaskHubAsync(tokenSource.Token).Returns(taskHub); - taskHub.IsHealthyAsync(tokenSource.Token).Returns(true); - - HealthCheckResult actual = await _healthCheck.CheckHealthAsync(new HealthCheckContext(), tokenSource.Token); - Assert.Equal(HealthStatus.Healthy, actual.Status); - - await _client.Received(1).GetTaskHubAsync(tokenSource.Token); - await taskHub.Received(1).IsHealthyAsync(tokenSource.Token); - } - - [Fact] - public async Task GivenPrerequisiteIsNotHealthy_WhenCheckingHealth_ThenReturnDegraded() - { - using var tokenSource = new CancellationTokenSource(); - _customerKeyHealthCache.Set(new CustomerKeyHealth - { - IsHealthy = false, - Reason = HealthStatusReason.CustomerManagedKeyAccessLost, - }); - - HealthCheckResult actual = await _healthCheck.CheckHealthAsync(new HealthCheckContext(), tokenSource.Token); - Assert.Equal(HealthStatus.Degraded, actual.Status); - } -} diff --git a/src/Microsoft.Health.Dicom.Functions.Client.UnitTests/Microsoft.Health.Dicom.Functions.Client.UnitTests.csproj b/src/Microsoft.Health.Dicom.Functions.Client.UnitTests/Microsoft.Health.Dicom.Functions.Client.UnitTests.csproj deleted file mode 100644 index 274b81a280..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Client.UnitTests/Microsoft.Health.Dicom.Functions.Client.UnitTests.csproj +++ /dev/null @@ -1,30 +0,0 @@ - - - - $(LibraryFrameworks) - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Microsoft.Health.Dicom.Functions.Client.UnitTests/TaskHub/AzureStorageTaskHubClientTests.cs b/src/Microsoft.Health.Dicom.Functions.Client.UnitTests/TaskHub/AzureStorageTaskHubClientTests.cs deleted file mode 100644 index f8f7093067..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Client.UnitTests/TaskHub/AzureStorageTaskHubClientTests.cs +++ /dev/null @@ -1,56 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using Azure.Data.Tables; -using Azure.Storage.Blobs; -using Azure.Storage.Queues; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Health.Dicom.Functions.Client.TaskHub; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Functions.Client.UnitTests.TaskHub; - -public class AzureStorageTaskHubClientTests -{ - private readonly LeasesContainer _leasesContainer = Substitute.For(Substitute.For("UseDevelopmentStorage=true"), "Foo"); - private readonly AzureStorageTaskHubClient _client; - - public AzureStorageTaskHubClientTests() - { - _client = new AzureStorageTaskHubClient( - _leasesContainer, - Substitute.For("UseDevelopmentStorage=true"), - Substitute.For("UseDevelopmentStorage=true"), - NullLoggerFactory.Instance); - } - - [Fact] - public async Task GivenMissingLeases_WhenGettingTaskHub_ThenReturnNull() - { - using var tokenSource = new CancellationTokenSource(); - - _leasesContainer.GetTaskHubInfoAsync(tokenSource.Token).Returns((TaskHubInfo)null); - - Assert.Null(await _client.GetTaskHubAsync(tokenSource.Token)); - - await _leasesContainer.Received(1).GetTaskHubInfoAsync(tokenSource.Token); - } - - [Fact] - public async Task GivenAvailableLeases_WhenGettingTaskHub_ThenReturnObject() - { - using var tokenSource = new CancellationTokenSource(); - var taskHubInfo = new TaskHubInfo { PartitionCount = 4, TaskHubName = "TestTaskHub" }; - - _leasesContainer.GetTaskHubInfoAsync(tokenSource.Token).Returns(taskHubInfo); - - Assert.IsType(await _client.GetTaskHubAsync(tokenSource.Token)); - - await _leasesContainer.Received(1).GetTaskHubInfoAsync(tokenSource.Token); - } -} diff --git a/src/Microsoft.Health.Dicom.Functions.Client.UnitTests/TaskHub/AzureStorageTaskHubTests.cs b/src/Microsoft.Health.Dicom.Functions.Client.UnitTests/TaskHub/AzureStorageTaskHubTests.cs deleted file mode 100644 index 4e91bc31f1..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Client.UnitTests/TaskHub/AzureStorageTaskHubTests.cs +++ /dev/null @@ -1,118 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using Azure.Data.Tables; -using Azure.Storage.Queues; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Health.Dicom.Functions.Client.TaskHub; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Functions.Client.UnitTests.TaskHub; - -public class AzureStorageTaskHubTests -{ - private readonly ControlQueueCollection _controlQueues = Substitute.For(Substitute.For("UseDevelopmentStorage=true"), new TaskHubInfo()); - private readonly WorkItemQueue _workItemQueue = Substitute.For(Substitute.For("UseDevelopmentStorage=true"), "Foo"); - private readonly InstanceTable _instanceTable = Substitute.For(Substitute.For("UseDevelopmentStorage=true"), "Foo"); - private readonly HistoryTable _historyTable = Substitute.For(Substitute.For("UseDevelopmentStorage=true"), "Foo"); - private readonly AzureStorageTaskHub _taskHub; - - public AzureStorageTaskHubTests() - { - _taskHub = new AzureStorageTaskHub( - _controlQueues, - _workItemQueue, - _instanceTable, - _historyTable, - NullLogger.Instance); - } - - [Fact] - public async Task GivenMissingControlQueues_WhenCheckingHealth_ThenReturnFalse() - { - using var tokenSource = new CancellationTokenSource(); - - _controlQueues.ExistAsync(tokenSource.Token).Returns(false); - - Assert.False(await _taskHub.IsHealthyAsync(tokenSource.Token)); - - await _controlQueues.Received(1).ExistAsync(tokenSource.Token); - await _workItemQueue.Received(1).ExistsAsync(Arg.Any()); - await _instanceTable.Received(1).ExistsAsync(Arg.Any()); - await _historyTable.Received(1).ExistsAsync(Arg.Any()); - } - - [Fact] - public async Task GivenMissingWorkItemQueue_WhenCheckingHealth_ThenReturnFalse() - { - using var tokenSource = new CancellationTokenSource(); - - _controlQueues.ExistAsync(tokenSource.Token).Returns(true); - _workItemQueue.ExistsAsync(tokenSource.Token).Returns(false); - - Assert.False(await _taskHub.IsHealthyAsync(tokenSource.Token)); - - await _controlQueues.Received(1).ExistAsync(tokenSource.Token); - await _workItemQueue.Received(1).ExistsAsync(Arg.Any()); - await _instanceTable.Received(1).ExistsAsync(Arg.Any()); - await _historyTable.Received(1).ExistsAsync(Arg.Any()); - } - - [Fact] - public async Task GivenMissingInstanceTable_WhenCheckingHealth_ThenReturnFalse() - { - using var tokenSource = new CancellationTokenSource(); - - _controlQueues.ExistAsync(tokenSource.Token).Returns(true); - _workItemQueue.ExistsAsync(tokenSource.Token).Returns(true); - _instanceTable.ExistsAsync(tokenSource.Token).Returns(false); - - Assert.False(await _taskHub.IsHealthyAsync(tokenSource.Token)); - - await _controlQueues.Received(1).ExistAsync(tokenSource.Token); - await _workItemQueue.Received(1).ExistsAsync(Arg.Any()); - await _instanceTable.Received(1).ExistsAsync(Arg.Any()); - await _historyTable.Received(1).ExistsAsync(Arg.Any()); - } - - [Fact] - public async Task GivenMissingHistoryTable_WhenCheckingHealth_ThenReturnFalse() - { - using var tokenSource = new CancellationTokenSource(); - - _controlQueues.ExistAsync(tokenSource.Token).Returns(true); - _workItemQueue.ExistsAsync(tokenSource.Token).Returns(true); - _instanceTable.ExistsAsync(tokenSource.Token).Returns(true); - _historyTable.ExistsAsync(tokenSource.Token).Returns(false); - - Assert.False(await _taskHub.IsHealthyAsync(tokenSource.Token)); - - await _controlQueues.Received(1).ExistAsync(tokenSource.Token); - await _workItemQueue.Received(1).ExistsAsync(Arg.Any()); - await _instanceTable.Received(1).ExistsAsync(Arg.Any()); - await _historyTable.Received(1).ExistsAsync(Arg.Any()); - } - - [Fact] - public async Task GivenAvailableTaskHub_WhenCheckingHealth_ThenReturnTrue() - { - using var tokenSource = new CancellationTokenSource(); - - _controlQueues.ExistAsync(tokenSource.Token).Returns(true); - _workItemQueue.ExistsAsync(tokenSource.Token).Returns(true); - _instanceTable.ExistsAsync(tokenSource.Token).Returns(true); - _historyTable.ExistsAsync(tokenSource.Token).Returns(true); - - Assert.True(await _taskHub.IsHealthyAsync(tokenSource.Token)); - - await _controlQueues.Received(1).ExistAsync(tokenSource.Token); - await _workItemQueue.Received(1).ExistsAsync(Arg.Any()); - await _instanceTable.Received(1).ExistsAsync(Arg.Any()); - await _historyTable.Received(1).ExistsAsync(Arg.Any()); - } -} diff --git a/src/Microsoft.Health.Dicom.Functions.Client.UnitTests/TaskHub/BlobDownloadResultFactory.cs b/src/Microsoft.Health.Dicom.Functions.Client.UnitTests/TaskHub/BlobDownloadResultFactory.cs deleted file mode 100644 index ef704b5721..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Client.UnitTests/TaskHub/BlobDownloadResultFactory.cs +++ /dev/null @@ -1,42 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Linq.Expressions; -using System.Reflection; -using Azure.Storage.Blobs.Models; - -namespace Microsoft.Health.Dicom.Functions.Client.UnitTests.TaskHub; - -internal static class BlobDownloadResultFactory -{ - private static readonly Func DownloadResultFactory = CreateFactory(); - - public static BlobDownloadResult CreateResult(BinaryData data) - => DownloadResultFactory(data); - - private static Func CreateFactory() - { - ParameterExpression binaryParam = Expression.Parameter(typeof(BinaryData), "binaryData"); - ParameterExpression resultsVar = Expression.Variable(typeof(BlobDownloadResult), "results"); - - return Expression - .Lambda>( - Expression.Block( - typeof(BlobDownloadResult), - new ParameterExpression[] { resultsVar }, - Expression.Assign( - resultsVar, - Expression.New( - typeof(BlobDownloadResult).GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, Type.EmptyTypes)!)), - Expression.Call( - resultsVar, - typeof(BlobDownloadResult).GetProperty(nameof(BlobDownloadResult.Content))!.GetSetMethod(nonPublic: true)!, - binaryParam), - resultsVar), - binaryParam) - .Compile(); - } -} diff --git a/src/Microsoft.Health.Dicom.Functions.Client.UnitTests/TaskHub/ControlQueueCollectionTests.cs b/src/Microsoft.Health.Dicom.Functions.Client.UnitTests/TaskHub/ControlQueueCollectionTests.cs deleted file mode 100644 index 55308b741e..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Client.UnitTests/TaskHub/ControlQueueCollectionTests.cs +++ /dev/null @@ -1,93 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Azure; -using Azure.Storage.Queues; -using Microsoft.Health.Dicom.Functions.Client.TaskHub; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Functions.Client.UnitTests.TaskHub; - -public class ControlQueueCollectionTests -{ - private readonly TaskHubInfo _taskHubInfo = new TaskHubInfo { PartitionCount = PartitionCount, TaskHubName = TaskHubName }; - private readonly QueueServiceClient _queueServiceClient = Substitute.For("UseDevelopmentStorage=true"); - private readonly QueueClient[] _queueClients; - private readonly ControlQueueCollection _controlQueues; - - private const int PartitionCount = 3; - private const string TaskHubName = "TestTaskHub"; - - public static IEnumerable EnumeratePartitions => Enumerable - .Repeat(null, PartitionCount) - .Select((obj, i) => new object[] { i }); - - public ControlQueueCollectionTests() - { - _queueClients = new QueueClient[_taskHubInfo.PartitionCount]; - for (int i = 0; i < _queueClients.Length; i++) - { - string name = ControlQueueCollection.GetName(TaskHubName, i); - _queueClients[i] = Substitute.For("UseDevelopmentStorage=true", name); - _queueClients[i].Name.Returns(name); - - _queueServiceClient.GetQueueClient(name).Returns(_queueClients[i]); - } - - _controlQueues = new ControlQueueCollection(_queueServiceClient, _taskHubInfo); - } - - [Theory] - [MemberData(nameof(EnumeratePartitions))] - public async Task GivenOneMissingQueue_WhenCheckingExistence_ThenReturnFalse(int missingPartitionIndex) - { - // Set up clients - using var tokenSource = new CancellationTokenSource(); - - for (int i = 0; i < _queueClients.Length; i++) - { - _queueClients[i] - .ExistsAsync(tokenSource.Token) - .Returns(Task.FromResult(Response.FromValue(i != missingPartitionIndex, Substitute.For()))); - } - - // Test - Assert.False(await _controlQueues.ExistAsync(tokenSource.Token)); - - for (int i = 0; i < _queueClients.Length; i++) - { - _queueServiceClient.Received(1).GetQueueClient(ControlQueueCollection.GetName(TaskHubName, i)); - await _queueClients[i].Received(1).ExistsAsync(tokenSource.Token); - } - } - - [Fact] - public async Task GivenAvailableQueues_WhenCheckingExistence_ThenReturnTrue() - { - // Set up clients - using var tokenSource = new CancellationTokenSource(); - - for (int i = 0; i < _queueClients.Length; i++) - { - _queueClients[i] - .ExistsAsync(tokenSource.Token) - .Returns(Task.FromResult(Response.FromValue(true, Substitute.For()))); - } - - // Test - Assert.True(await _controlQueues.ExistAsync(tokenSource.Token)); - - for (int i = 0; i < _queueClients.Length; i++) - { - _queueServiceClient.Received(1).GetQueueClient(ControlQueueCollection.GetName(TaskHubName, i)); - await _queueClients[i].Received(1).ExistsAsync(tokenSource.Token); - } - } -} diff --git a/src/Microsoft.Health.Dicom.Functions.Client.UnitTests/TaskHub/HistoryTableTests.cs b/src/Microsoft.Health.Dicom.Functions.Client.UnitTests/TaskHub/HistoryTableTests.cs deleted file mode 100644 index 8ad2726df4..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Client.UnitTests/TaskHub/HistoryTableTests.cs +++ /dev/null @@ -1,20 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Azure.Data.Tables; -using System.Threading.Tasks; -using System.Threading; -using Microsoft.Health.Dicom.Functions.Client.TaskHub; - -namespace Microsoft.Health.Dicom.Functions.Client.UnitTests.TaskHub; - -public class HistoryTableTests : TrackingTableTests -{ - protected override ValueTask ExistsAsync(TableServiceClient tableServiceClient, string tableName, CancellationToken cancellationToken) - => new HistoryTable(tableServiceClient, tableName).ExistsAsync(cancellationToken); - - protected override string GetName(string taskHubName) - => HistoryTable.GetName(taskHubName); -} diff --git a/src/Microsoft.Health.Dicom.Functions.Client.UnitTests/TaskHub/InstanceTableTests.cs b/src/Microsoft.Health.Dicom.Functions.Client.UnitTests/TaskHub/InstanceTableTests.cs deleted file mode 100644 index e108de55f2..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Client.UnitTests/TaskHub/InstanceTableTests.cs +++ /dev/null @@ -1,20 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using Azure.Data.Tables; -using Microsoft.Health.Dicom.Functions.Client.TaskHub; - -namespace Microsoft.Health.Dicom.Functions.Client.UnitTests.TaskHub; - -public class InstanceTableTests : TrackingTableTests -{ - protected override ValueTask ExistsAsync(TableServiceClient tableServiceClient, string taskHubName, CancellationToken cancellationToken) - => new InstanceTable(tableServiceClient, taskHubName).ExistsAsync(cancellationToken); - - protected override string GetName(string taskHubName) - => InstanceTable.GetName(taskHubName); -} diff --git a/src/Microsoft.Health.Dicom.Functions.Client.UnitTests/TaskHub/LeasesContainerTests.cs b/src/Microsoft.Health.Dicom.Functions.Client.UnitTests/TaskHub/LeasesContainerTests.cs deleted file mode 100644 index f3dacdf250..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Client.UnitTests/TaskHub/LeasesContainerTests.cs +++ /dev/null @@ -1,82 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Threading; -using System.Threading.Tasks; -using Azure; -using Azure.Storage.Blobs; -using Azure.Storage.Blobs.Models; -using Microsoft.Health.Dicom.Functions.Client.TaskHub; -using NSubstitute; -using Xunit; -using System.Text.Json; -using System.Net; - -namespace Microsoft.Health.Dicom.Functions.Client.UnitTests.TaskHub; - -public class LeasesContainerTests -{ - private readonly string _containerName = LeasesContainer.GetName(TaskHubName); - private readonly BlobServiceClient _blobServiceClient = Substitute.For("UseDevelopmentStorage=true"); - private readonly BlobContainerClient _blobContainerClient; - private readonly BlobClient _blobClient; - private readonly LeasesContainer _leasesContainer; - - private const string TaskHubName = "TestTaskHub"; - - public LeasesContainerTests() - { - _blobContainerClient = Substitute.For("UseDevelopmentStorage=true", _containerName); - _blobClient = Substitute.For("UseDevelopmentStorage=true", _containerName, LeasesContainer.TaskHubBlobName); - _blobServiceClient.GetBlobContainerClient(_containerName).Returns(_blobContainerClient); - _blobContainerClient.GetBlobClient(LeasesContainer.TaskHubBlobName).Returns(_blobClient); - _leasesContainer = new LeasesContainer(_blobServiceClient, TaskHubName); - } - - [Fact] - public async Task GivenMissingContainerOrBlob_WhenGettingInfo_ThenReturnNull() - { - // Set up clients - using var tokenSource = new CancellationTokenSource(); - _blobClient - .DownloadContentAsync(tokenSource.Token) - .Returns( - Task.FromException>( - new RequestFailedException((int)HttpStatusCode.NotFound, "Blob not found."))); - - // Test - Assert.Null(await _leasesContainer.GetTaskHubInfoAsync(tokenSource.Token)); - - _blobServiceClient.Received(1).GetBlobContainerClient(_containerName); - _blobContainerClient.Received(1).GetBlobClient(LeasesContainer.TaskHubBlobName); - await _blobClient.Received(1).DownloadContentAsync(tokenSource.Token); - } - - [Fact] - public async Task GivenAvailableBlob_WhenGettingInfo_ThenReturnObject() - { - // Set up clients - using var tokenSource = new CancellationTokenSource(); - - var expected = new TaskHubInfo { CreatedAt = DateTime.UtcNow, PartitionCount = 7, TaskHubName = TaskHubName }; - BlobDownloadResult result = BlobDownloadResultFactory.CreateResult(new BinaryData(JsonSerializer.Serialize(expected))); - _blobClient - .DownloadContentAsync(tokenSource.Token) - .Returns(Response.FromValue(result, Substitute.For())); - - // Test - TaskHubInfo actual = await _leasesContainer.GetTaskHubInfoAsync(tokenSource.Token); - - Assert.NotNull(actual); - Assert.Equal(expected.CreatedAt, actual.CreatedAt); - Assert.Equal(expected.PartitionCount, actual.PartitionCount); - Assert.Equal(expected.TaskHubName, actual.TaskHubName); - - _blobServiceClient.Received(1).GetBlobContainerClient(_containerName); - _blobContainerClient.Received(1).GetBlobClient(LeasesContainer.TaskHubBlobName); - await _blobClient.Received(1).DownloadContentAsync(tokenSource.Token); - } -} diff --git a/src/Microsoft.Health.Dicom.Functions.Client.UnitTests/TaskHub/TrackingTableTests.cs b/src/Microsoft.Health.Dicom.Functions.Client.UnitTests/TaskHub/TrackingTableTests.cs deleted file mode 100644 index 71ac108531..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Client.UnitTests/TaskHub/TrackingTableTests.cs +++ /dev/null @@ -1,80 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using Azure; -using Azure.Data.Tables; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Functions.Client.UnitTests.TaskHub; - -public abstract class TrackingTableTests -{ - private readonly string _tableName; - private readonly TableServiceClient _tableServiceClient = Substitute.For("UseDevelopmentStorage=true"); - private readonly TableClient _tableClient; - private readonly AsyncPageable _asyncPageable = Substitute.For>(); - private readonly IAsyncEnumerator _asyncEnumerator = Substitute.For>(); - - private const string EmptyQuery = "PartitionKey eq ''"; - private const string TaskHubName = "TestTaskHub"; - - protected TrackingTableTests() - { - _tableName = GetName(TaskHubName); - _tableClient = Substitute.For("UseDevelopmentStorage=true", _tableName); - _tableServiceClient.GetTableClient(_tableName).Returns(_tableClient); - } - - [Fact] - public async Task GivenMissingTable_WhenCheckingExistence_ThenReturnFalse() - { - // Set up clients - using var tokenSource = new CancellationTokenSource(); - - _tableClient.QueryAsync(EmptyQuery, 1, null, tokenSource.Token).Returns(_asyncPageable); - _asyncPageable.GetAsyncEnumerator(tokenSource.Token).Returns(_asyncEnumerator); - _asyncEnumerator - .MoveNextAsync(tokenSource.Token) - .Returns(info => ValueTask.FromException(new RequestFailedException((int)HttpStatusCode.NotFound, "Cannot find table"))); - - // Test - Assert.False(await ExistsAsync(_tableServiceClient, TaskHubName, tokenSource.Token)); - - _tableServiceClient.Received(1).GetTableClient(_tableName); - _tableClient.Received(1).QueryAsync(EmptyQuery, 1, null, tokenSource.Token); - _asyncPageable.Received(1).GetAsyncEnumerator(tokenSource.Token); - await _asyncEnumerator.Received(1).MoveNextAsync(tokenSource.Token); - } - - [Fact] - public async Task GivenAvailableTable_WhenCheckingExistence_ThenReturnTrue() - { - // Set up clients - using var tokenSource = new CancellationTokenSource(); - - _tableClient.QueryAsync(EmptyQuery, 1, null, tokenSource.Token).Returns(_asyncPageable); - _asyncPageable.GetAsyncEnumerator(tokenSource.Token).Returns(_asyncEnumerator); - _asyncEnumerator - .MoveNextAsync(tokenSource.Token) - .Returns(info => ValueTask.FromResult(true)); - - // Test - Assert.True(await ExistsAsync(_tableServiceClient, TaskHubName, tokenSource.Token)); - - _tableServiceClient.Received(1).GetTableClient(_tableName); - _tableClient.Received(1).QueryAsync(EmptyQuery, 1, null, tokenSource.Token); - _asyncPageable.Received(1).GetAsyncEnumerator(tokenSource.Token); - await _asyncEnumerator.Received(1).MoveNextAsync(tokenSource.Token); - } - - protected abstract ValueTask ExistsAsync(TableServiceClient tableServiceClient, string tableName, CancellationToken cancellationToken); - - protected abstract string GetName(string taskHubName); -} diff --git a/src/Microsoft.Health.Dicom.Functions.Client.UnitTests/TaskHub/WorkItemQueueTests.cs b/src/Microsoft.Health.Dicom.Functions.Client.UnitTests/TaskHub/WorkItemQueueTests.cs deleted file mode 100644 index db9c23ffb2..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Client.UnitTests/TaskHub/WorkItemQueueTests.cs +++ /dev/null @@ -1,63 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using Azure; -using Azure.Storage.Queues; -using Microsoft.Health.Dicom.Functions.Client.TaskHub; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Functions.Client.UnitTests.TaskHub; - -public class WorkItemQueueTests -{ - private readonly string _queueName = WorkItemQueue.GetName(TaskHubName); - private readonly QueueServiceClient _queueServiceClient = Substitute.For("UseDevelopmentStorage=true"); - private readonly QueueClient _queueClient; - private readonly WorkItemQueue _workItemQueue; - - private const string TaskHubName = "TestTaskHub"; - - public WorkItemQueueTests() - { - _queueClient = Substitute.For("UseDevelopmentStorage=true", _queueName); - _queueServiceClient.GetQueueClient(_queueName).Returns(_queueClient); - _workItemQueue = new WorkItemQueue(_queueServiceClient, TaskHubName); - } - - [Fact] - public async Task GivenMissingQueue_WhenCheckingExistence_ThenReturnFalse() - { - // Set up clients - using var tokenSource = new CancellationTokenSource(); - _queueClient - .ExistsAsync(tokenSource.Token) - .Returns(Response.FromValue(false, Substitute.For())); - - // Test - Assert.False(await _workItemQueue.ExistsAsync(tokenSource.Token)); - - _queueServiceClient.Received(1).GetQueueClient(_queueName); - await _queueClient.Received(1).ExistsAsync(tokenSource.Token); - } - - [Fact] - public async Task GivenAvailableQueue_WhenCheckingExistence_ThenReturnTrue() - { - // Set up clients - using var tokenSource = new CancellationTokenSource(); - _queueClient - .ExistsAsync(tokenSource.Token) - .Returns(Response.FromValue(true, Substitute.For())); - - // Test - Assert.True(await _workItemQueue.ExistsAsync(tokenSource.Token)); - - _queueServiceClient.Received(1).GetQueueClient(_queueName); - await _queueClient.Received(1).ExistsAsync(tokenSource.Token); - } -} diff --git a/src/Microsoft.Health.Dicom.Functions.Client/DicomAzureFunctionsClient.cs b/src/Microsoft.Health.Dicom.Functions.Client/DicomAzureFunctionsClient.cs deleted file mode 100644 index abe5ff1961..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Client/DicomAzureFunctionsClient.cs +++ /dev/null @@ -1,324 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Microsoft.Azure.WebJobs.Extensions.DurableTask.ContextImplementations; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Features.Operations; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Core.Features.Routing; -using Microsoft.Health.Dicom.Core.Models.Export; -using Microsoft.Health.Dicom.Core.Models.Operations; -using Microsoft.Health.Dicom.Core.Models.Update; -using Microsoft.Health.Dicom.Functions.Client.Extensions; -using Microsoft.Health.Dicom.Functions.ContentLengthBackFill; -using Microsoft.Health.Dicom.Functions.DataCleanup; -using Microsoft.Health.Dicom.Functions.Export; -using Microsoft.Health.Dicom.Functions.Indexing; -using Microsoft.Health.Dicom.Functions.Update; -using Microsoft.Health.Operations; -using Microsoft.Health.Operations.Functions.DurableTask; - -namespace Microsoft.Health.Dicom.Functions.Client; - -/// -/// Represents a client for interacting with DICOM-specific Azure Functions. -/// -internal class DicomAzureFunctionsClient : IDicomOperationsClient -{ - private readonly IDurableClient _durableClient; - private readonly IUrlResolver _urlResolver; - private readonly IDicomOperationsResourceStore _resourceStore; - private readonly DicomFunctionOptions _options; - private readonly ILogger _logger; - private readonly JsonSerializerOptions _jsonSerializerOptions; - - /// - /// Initializes a new instance of the class. - /// - /// The client for interacting with durable functions. - /// A helper for building URLs for other APIs. - /// A store for resolving DICOM resources that are the subject of operations. - /// Options for configuring the functions. - /// Json serialization options - /// A logger for diagnostic information. - /// - /// , , or - /// is . - /// - public DicomAzureFunctionsClient( - IDurableClientFactory durableClientFactory, - IUrlResolver urlResolver, - IDicomOperationsResourceStore resourceStore, - IOptions options, - IOptions jsonSerializerOptions, - ILogger logger) - { - _durableClient = EnsureArg.IsNotNull(durableClientFactory, nameof(durableClientFactory)).CreateClient(); - _urlResolver = EnsureArg.IsNotNull(urlResolver, nameof(urlResolver)); - _resourceStore = EnsureArg.IsNotNull(resourceStore, nameof(resourceStore)); - _options = EnsureArg.IsNotNull(options?.Value, nameof(options)); - _jsonSerializerOptions = EnsureArg.IsNotNull(jsonSerializerOptions?.Value, nameof(jsonSerializerOptions)); - _logger = EnsureArg.IsNotNull(logger, nameof(logger)); - } - - /// - public Task> GetStateAsync(Guid operationId, CancellationToken cancellationToken = default) - => GetStateAsync>( - operationId, - async (operation, state, checkpoint, token) => - { - OperationStatus status = state.RuntimeStatus.ToOperationStatus(); - return new OperationState - { - CreatedTime = checkpoint.CreatedTime ?? state.CreatedTime, - LastUpdatedTime = state.LastUpdatedTime, - OperationId = operationId, - PercentComplete = checkpoint.PercentComplete.HasValue && status == OperationStatus.Succeeded ? 100 : checkpoint.PercentComplete, - Resources = await GetResourceUrlsAsync(operation, checkpoint.ResourceIds, cancellationToken), - Results = checkpoint.GetResults(state.Output), - Status = status, - Type = operation, - }; - }, - cancellationToken); - - /// - public async IAsyncEnumerable FindOperationsAsync(OperationQueryCondition query, [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - EnsureArg.IsNotNull(query, nameof(query)); - - var operations = query.Operations == null ? null : new HashSet(query.Operations); - var result = new OrchestrationStatusQueryResult(); - do - { - result = await _durableClient.ListInstancesAsync( - query.ForDurableFunctions(result.ContinuationToken), - cancellationToken); - - IEnumerable matches = GetValidOperationIds(result - .DurableOrchestrationState - .Where(s => operations == null || operations.Contains(s.GetDicomOperation()))); - - foreach (Guid operationId in matches) - { - yield return new OperationReference(operationId, _urlResolver.ResolveOperationStatusUri(operationId)); - } - } while (result.ContinuationToken != null && !cancellationToken.IsCancellationRequested); - } - - /// - public Task> GetLastCheckpointAsync(Guid operationId, CancellationToken cancellationToken = default) - => GetStateAsync( - operationId, - (operation, state, checkpoint, token) => - Task.FromResult(new OperationCheckpointState - { - OperationId = operationId, - Status = state.RuntimeStatus.ToOperationStatus(), - Type = operation, - Checkpoint = checkpoint - }), - cancellationToken); - - /// - public async Task StartReindexingInstancesAsync(Guid operationId, IReadOnlyCollection tagKeys, CancellationToken cancellationToken = default) - { - EnsureArg.IsNotNull(tagKeys, nameof(tagKeys)); - EnsureArg.HasItems(tagKeys, nameof(tagKeys)); - - cancellationToken.ThrowIfCancellationRequested(); - - // TODO: Pass token when supported - string instanceId = await _durableClient.StartNewAsync( - _options.Indexing.Name, - operationId.ToString(OperationId.FormatSpecifier), - new ReindexInput - { - Batching = _options.Indexing.Batching, - QueryTagKeys = tagKeys - }); - - _logger.LogInformation("Successfully started new re-index orchestration instance with ID '{InstanceId}'.", instanceId); - - return new OperationReference(operationId, _urlResolver.ResolveOperationStatusUri(operationId)); - } - - /// - public async Task StartExportAsync(Guid operationId, ExportSpecification specification, Uri errorHref, Partition partition, CancellationToken cancellationToken = default) - { - EnsureArg.IsNotNull(specification, nameof(specification)); - EnsureArg.IsNotNull(errorHref, nameof(errorHref)); - EnsureArg.IsNotNull(partition, nameof(partition)); - - cancellationToken.ThrowIfCancellationRequested(); - - // TODO: Pass token when supported - string instanceId = await _durableClient.StartNewAsync( - _options.Export.Name, - operationId.ToString(OperationId.FormatSpecifier), - new ExportInput - { - Batching = _options.Export.Batching, - Destination = specification.Destination, - ErrorHref = errorHref, - Partition = partition, - Source = specification.Source, - }); - - _logger.LogInformation("Successfully started new export orchestration instance with ID '{InstanceId}'.", instanceId); - - return new OperationReference(operationId, _urlResolver.ResolveOperationStatusUri(operationId)); - } - - /// - public async Task StartUpdateOperationAsync(Guid operationId, UpdateSpecification updateSpecification, Partition partition, CancellationToken cancellationToken = default) - { - EnsureArg.IsNotNull(updateSpecification, nameof(updateSpecification)); - - cancellationToken.ThrowIfCancellationRequested(); - - string datasetToUpdate = JsonSerializer.Serialize(updateSpecification.ChangeDataset, _jsonSerializerOptions); - string instanceId = await _durableClient.StartNewAsync( - _options.Update.Name, - operationId.ToString(OperationId.FormatSpecifier), - new UpdateInput - { - Partition = partition, - ChangeDataset = datasetToUpdate, - StudyInstanceUids = updateSpecification.StudyInstanceUids, - }); - - _logger.LogInformation("Successfully started new update operation instance with ID '{InstanceId}'.", instanceId); - - return new OperationReference(operationId, _urlResolver.ResolveOperationStatusUri(operationId)); - } - - /// - public async Task StartInstanceDataCleanupOperationAsync(Guid operationId, DateTimeOffset startFilterTimeStamp, DateTimeOffset endFilterTimeStamp, CancellationToken cancellationToken = default) - { - EnsureArg.IsGt(endFilterTimeStamp, startFilterTimeStamp, nameof(endFilterTimeStamp)); - - cancellationToken.ThrowIfCancellationRequested(); - - string instanceId = await _durableClient.StartNewAsync( - _options.DataCleanup.Name, - operationId.ToString(OperationId.FormatSpecifier), - new DataCleanupCheckPoint - { - Batching = _options.DataCleanup.Batching, - StartFilterTimeStamp = startFilterTimeStamp, - EndFilterTimeStamp = endFilterTimeStamp, - }); - - _logger.LogInformation("Successfully started data cleanup operation with ID '{InstanceId}'.", instanceId); - } - - /// - public async Task StartContentLengthBackFillOperationAsync(Guid operationId, CancellationToken cancellationToken = default) - { - cancellationToken.ThrowIfCancellationRequested(); - - string instanceId = await _durableClient.StartNewAsync( - _options.ContentLengthBackFill.Name, - operationId.ToString(OperationId.FormatSpecifier), - new ContentLengthBackFillCheckPoint() { Batching = _options.ContentLengthBackFill.Batching }); - - _logger.LogInformation("Successfully started content length backfill operation with ID '{InstanceId}'.", instanceId); - } - - private async Task GetStateAsync( - Guid operationId, - Func> factory, - CancellationToken cancellationToken) - where T : class - { - cancellationToken.ThrowIfCancellationRequested(); - - DurableOrchestrationStatus state = await _durableClient.GetStatusAsync(operationId.ToString(OperationId.FormatSpecifier), showInput: true); - - if (state == null) - { - return null; - } - - _logger.LogInformation( - "Successfully found the state of orchestration instance '{InstanceId}' with name '{Name}'.", - state.InstanceId, - state.Name); - - DicomOperation type = state.GetDicomOperation(); - if (type == DicomOperation.Unknown) - { - _logger.LogWarning("Orchestration instance with '{Name}' did not resolve to a public operation type.", state.Name); - return null; - } - - return await factory(type, state, ParseCheckpoint(type, state), cancellationToken); - } - - private async Task> GetResourceUrlsAsync( - DicomOperation type, - IReadOnlyCollection resourceIds, - CancellationToken cancellationToken) - { - switch (type) - { - case DicomOperation.Export: - case DicomOperation.DataCleanup: - case DicomOperation.ContentLengthBackFill: - return null; - case DicomOperation.Reindex: - IReadOnlyList tagPaths = Array.Empty(); - List tagKeys = resourceIds?.Select(x => int.Parse(x, CultureInfo.InvariantCulture)).ToList(); - if (tagKeys?.Count > 0) - { - tagPaths = await _resourceStore - .ResolveQueryTagKeysAsync(tagKeys, cancellationToken) - .Select(x => _urlResolver.ResolveQueryTagUri(x)) - .ToListAsync(cancellationToken); - } - - return tagPaths; - case DicomOperation.Update: - return null; - default: - throw new ArgumentOutOfRangeException(nameof(type)); - } - } - - // Note that the Durable Task Framework does not preserve the original CreatedTime - // when an orchestration is restarted via ContinueAsNew, so we may store the original - // in the checkpoint - private static IOrchestrationCheckpoint ParseCheckpoint(DicomOperation type, DurableOrchestrationStatus status) - => type switch - { - DicomOperation.DataCleanup => status.Input?.ToObject() ?? new DataCleanupCheckPoint(), - DicomOperation.ContentLengthBackFill => status.Input?.ToObject() ?? new ContentLengthBackFillCheckPoint(), - DicomOperation.Export => status.Input?.ToObject() ?? new ExportCheckpoint(), - DicomOperation.Reindex => status.Input?.ToObject() ?? new ReindexCheckpoint(), - DicomOperation.Update => status.Input?.ToObject() ?? new UpdateCheckpoint(), - _ => NullOrchestrationCheckpoint.Value, - }; - - private static IEnumerable GetValidOperationIds(IEnumerable statuses) - { - foreach (DurableOrchestrationStatus status in statuses) - { - if (Guid.TryParse(status.InstanceId, out Guid operationId)) - yield return operationId; - } - } -} diff --git a/src/Microsoft.Health.Dicom.Functions.Client/DicomFunctionOptions.cs b/src/Microsoft.Health.Dicom.Functions.Client/DicomFunctionOptions.cs deleted file mode 100644 index 2b1b43ff41..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Client/DicomFunctionOptions.cs +++ /dev/null @@ -1,32 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.ComponentModel.DataAnnotations; -using Microsoft.Azure.WebJobs.Extensions.DurableTask.Options; - -namespace Microsoft.Health.Dicom.Functions.Client; - -internal class DicomFunctionOptions -{ - public const string SectionName = "DicomFunctions"; - - [Required] - public FanOutFunctionOptions ContentLengthBackFill { get; set; } - - [Required] - public FanOutFunctionOptions DataCleanup { get; set; } - - [Required] - public DurableClientOptions DurableTask { get; set; } - - [Required] - public FanOutFunctionOptions Export { get; set; } - - [Required] - public FanOutFunctionOptions Indexing { get; set; } - - [Required] - public FanOutFunctionOptions Update { get; set; } -} diff --git a/src/Microsoft.Health.Dicom.Functions.Client/Extensions/AzureComponentFactoryExtensions.cs b/src/Microsoft.Health.Dicom.Functions.Client/Extensions/AzureComponentFactoryExtensions.cs deleted file mode 100644 index a3143ba52e..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Client/Extensions/AzureComponentFactoryExtensions.cs +++ /dev/null @@ -1,92 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Globalization; -using Azure.Core; -using Azure.Data.Tables; -using Azure.Storage.Blobs; -using Azure.Storage.Queues; -using EnsureThat; -using Microsoft.Extensions.Azure; -using Microsoft.Extensions.Configuration; - -namespace Microsoft.Health.Dicom.Functions.Client.Extensions; - -internal static class AzureComponentFactoryExtensions -{ - public static BlobServiceClient CreateBlobServiceClient(this AzureComponentFactory factory, IConfigurationSection configuration) - => factory.CreateClient( - configuration, - uriOptions => uriOptions.BlobServiceUri, - (uri, credential, clientOptions) => new BlobServiceClient(uri, credential, clientOptions)); - - public static QueueServiceClient CreateQueueServiceClient(this AzureComponentFactory factory, IConfigurationSection configuration) - => factory.CreateClient( - configuration, - uriOptions => uriOptions.QueueServiceUri, - (uri, credential, clientOptions) => new QueueServiceClient(uri, credential, clientOptions)); - - public static TableServiceClient CreateTableServiceClient(this AzureComponentFactory factory, IConfigurationSection configuration) - => factory.CreateClient( - configuration, - uriOptions => uriOptions.TableServiceUri, - (uri, credential, clientOptions) => new TableServiceClient(uri, credential, clientOptions)); - - private static TClient CreateClient( - this AzureComponentFactory factory, - IConfigurationSection configuration, - Func uriSelector, - Func clientFactory) - { - EnsureArg.IsNotNull(factory, nameof(factory)); - EnsureArg.IsNotNull(configuration, nameof(configuration)); - - TokenCredential credential = factory.CreateTokenCredential(configuration); - TOptions options = (TOptions)factory.CreateClientOptions(typeof(TOptions), null, configuration); - - if (configuration.Value is null) - { - StorageServiceUriOptions serviceUriOptions = configuration.Get(); - Uri serviceUri = serviceUriOptions is null ? null : uriSelector(serviceUriOptions); - if (serviceUri != null) - return clientFactory(serviceUri, credential, options); - } - - return (TClient)factory.CreateClient(typeof(TClient), configuration, credential, options); - } - - private sealed class StorageServiceUriOptions - { - private Uri _blobServiceUri; - private Uri _queueServiceUri; - private Uri _tableServiceUri; - - public Uri BlobServiceUri - { - get => _blobServiceUri ?? CreateStorageServiceUri("blob"); - set => _blobServiceUri = value; - } - - public Uri QueueServiceUri - { - get => _queueServiceUri ?? CreateStorageServiceUri("queue"); - set => _queueServiceUri = value; - } - - public Uri TableServiceUri - { - get => _tableServiceUri ?? CreateStorageServiceUri("table"); - set => _tableServiceUri = value; - } - - public string AccountName { get; set; } - - private Uri CreateStorageServiceUri(string service) - => string.IsNullOrEmpty(AccountName) - ? null - : new Uri(string.Format(CultureInfo.InvariantCulture, "https://{0}.{1}.core.windows.net", AccountName, service)); - } -} diff --git a/src/Microsoft.Health.Dicom.Functions.Client/Extensions/DurableOrchestrationStatusExtensions.cs b/src/Microsoft.Health.Dicom.Functions.Client/Extensions/DurableOrchestrationStatusExtensions.cs deleted file mode 100644 index af0bb27a80..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Client/Extensions/DurableOrchestrationStatusExtensions.cs +++ /dev/null @@ -1,39 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using EnsureThat; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Microsoft.Health.Dicom.Core.Models.Operations; - -namespace Microsoft.Health.Dicom.Functions.Client.Extensions; - -internal static class DurableOrchestrationStatusExtensions -{ - public static DicomOperation GetDicomOperation(this DurableOrchestrationStatus status) - { - EnsureArg.IsNotNull(status, nameof(status)); - - if (status?.Name != null) - { - if (status.Name.StartsWith(FunctionNames.ContentLengthBackFill, StringComparison.OrdinalIgnoreCase)) - return DicomOperation.ContentLengthBackFill; - - if (status.Name.StartsWith(FunctionNames.DataCleanup, StringComparison.OrdinalIgnoreCase)) - return DicomOperation.DataCleanup; - - if (status.Name.StartsWith(FunctionNames.ExportDicomFiles, StringComparison.OrdinalIgnoreCase)) - return DicomOperation.Export; - - if (status.Name.StartsWith(FunctionNames.ReindexInstances, StringComparison.OrdinalIgnoreCase)) - return DicomOperation.Reindex; - - if (status.Name.StartsWith(FunctionNames.UpdateInstances, StringComparison.OrdinalIgnoreCase)) - return DicomOperation.Update; - } - - return DicomOperation.Unknown; - } -} diff --git a/src/Microsoft.Health.Dicom.Functions.Client/Extensions/OperationQueryConditionExtensions.cs b/src/Microsoft.Health.Dicom.Functions.Client/Extensions/OperationQueryConditionExtensions.cs deleted file mode 100644 index 31fe27f15a..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Client/Extensions/OperationQueryConditionExtensions.cs +++ /dev/null @@ -1,38 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Linq; -using EnsureThat; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Microsoft.Health.Dicom.Core.Models.Operations; -using Microsoft.Health.Operations.Functions.DurableTask; - -namespace Microsoft.Health.Dicom.Functions.Client.Extensions; - -internal static class OperationQueryConditionExtensions -{ - public static OrchestrationStatusQueryCondition ForDurableFunctions(this OperationQueryCondition query, string continuationToken = null) - { - // TODO #73705: Modify page size when we add /operations endpoint - EnsureArg.IsNotNull(query, nameof(query)); - - // Aggressively resolve to validate input - var statuses = query - .Statuses - .SelectMany(s => s.ToOrchestrationRuntimeStatuses()) - .Select(s => s != OrchestrationRuntimeStatus.Unknown ? s : throw new ArgumentOutOfRangeException(nameof(query))) - .ToList(); - - return new OrchestrationStatusQueryCondition - { - ContinuationToken = continuationToken, - CreatedTimeFrom = query.CreatedTimeFrom, - CreatedTimeTo = query.CreatedTimeTo, - RuntimeStatus = statuses, - ShowInput = true, - }; - } -} diff --git a/src/Microsoft.Health.Dicom.Functions.Client/FanOutFunctionOptions.cs b/src/Microsoft.Health.Dicom.Functions.Client/FanOutFunctionOptions.cs deleted file mode 100644 index 0983aff06d..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Client/FanOutFunctionOptions.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.ComponentModel.DataAnnotations; - -namespace Microsoft.Health.Dicom.Functions.Client; - -internal class FanOutFunctionOptions : FunctionOptions -{ - [Required] - public BatchingOptions Batching { get; set; } -} diff --git a/src/Microsoft.Health.Dicom.Functions.Client/FunctionNames.cs b/src/Microsoft.Health.Dicom.Functions.Client/FunctionNames.cs deleted file mode 100644 index a708662a56..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Client/FunctionNames.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Functions.Client; - -internal static class FunctionNames -{ - public const string ContentLengthBackFill = "ContentLengthBackFill"; - - public const string DataCleanup = "DataCleanup"; - - public const string ExportDicomFiles = "ExportDicomFiles"; - - public const string ReindexInstances = "ReindexInstances"; - - public const string UpdateInstances = "UpdateInstances"; -} diff --git a/src/Microsoft.Health.Dicom.Functions.Client/FunctionOptions.cs b/src/Microsoft.Health.Dicom.Functions.Client/FunctionOptions.cs deleted file mode 100644 index b4883e4119..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Client/FunctionOptions.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.ComponentModel.DataAnnotations; - -namespace Microsoft.Health.Dicom.Functions.Client; - -internal class FunctionOptions -{ - [Required] - public string Name { get; set; } -} diff --git a/src/Microsoft.Health.Dicom.Functions.Client/HealthChecks/DurableTaskHealthCheck.cs b/src/Microsoft.Health.Dicom.Functions.Client/HealthChecks/DurableTaskHealthCheck.cs deleted file mode 100644 index fdfc2b285a..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Client/HealthChecks/DurableTaskHealthCheck.cs +++ /dev/null @@ -1,44 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.Extensions.Diagnostics.HealthChecks; -using Microsoft.Extensions.Logging; -using Microsoft.Health.Core.Features.Health; -using Microsoft.Health.Dicom.Functions.Client.TaskHub; -using Microsoft.Health.Encryption.Customer.Health; - -namespace Microsoft.Health.Dicom.Functions.Client.HealthChecks; - -internal sealed class DurableTaskHealthCheck : AzureStorageHealthCheck -{ - private readonly ITaskHubClient _client; - private readonly ILogger _logger; - - public DurableTaskHealthCheck( - ITaskHubClient client, - ValueCache customerKeyHealthCache, - ILogger logger) - : base(customerKeyHealthCache, logger) - { - _client = EnsureArg.IsNotNull(client, nameof(client)); - _logger = EnsureArg.IsNotNull(logger, nameof(logger)); - } - - public override async Task CheckAzureStorageHealthAsync(CancellationToken cancellationToken) - { - ITaskHub taskHub = await _client.GetTaskHubAsync(cancellationToken).ConfigureAwait(false); - if (taskHub == null) - return HealthCheckResult.Unhealthy("Task hub not found."); - - if (!await taskHub.IsHealthyAsync(cancellationToken).ConfigureAwait(false)) - return HealthCheckResult.Unhealthy("Task hub is not ready."); - - _logger.LogInformation("Successfully connected to the task hub"); - return HealthCheckResult.Healthy("Successfully connected."); - } -} diff --git a/src/Microsoft.Health.Dicom.Functions.Client/Microsoft.Health.Dicom.Functions.Client.csproj b/src/Microsoft.Health.Dicom.Functions.Client/Microsoft.Health.Dicom.Functions.Client.csproj deleted file mode 100644 index 334ba48f2d..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Client/Microsoft.Health.Dicom.Functions.Client.csproj +++ /dev/null @@ -1,37 +0,0 @@ - - - - Defines a client for interacting with the DICOM Azure Functions. - $(LibraryFrameworks) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Microsoft.Health.Dicom.Functions.Client/Properties/AssemblyInfo.cs b/src/Microsoft.Health.Dicom.Functions.Client/Properties/AssemblyInfo.cs deleted file mode 100644 index 338390b416..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Client/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,13 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Resources; -using System.Runtime.CompilerServices; - -[assembly: CLSCompliant(false)] -[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] -[assembly: InternalsVisibleTo("Microsoft.Health.Dicom.Functions.Client.UnitTests")] -[assembly: NeutralResourcesLanguage("en-us")] diff --git a/src/Microsoft.Health.Dicom.Functions.Client/Registration/DicomServerBuilderFunctionClientRegistrationExtensions.cs b/src/Microsoft.Health.Dicom.Functions.Client/Registration/DicomServerBuilderFunctionClientRegistrationExtensions.cs deleted file mode 100644 index 7c16fa16c7..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Client/Registration/DicomServerBuilderFunctionClientRegistrationExtensions.cs +++ /dev/null @@ -1,77 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using EnsureThat; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Microsoft.Extensions.Azure; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Operations; -using Microsoft.Health.Dicom.Core.Registration; -using Microsoft.Health.Dicom.Functions.Client.HealthChecks; -using Microsoft.Health.Dicom.Functions.Client.TaskHub; -using Microsoft.Health.Operations.Functions.DurableTask; -using Newtonsoft.Json; - -namespace Microsoft.Health.Dicom.Functions.Client; - -/// -/// Provides a set of methods for adding services to the dependency injection -/// service container that are necessary for an Azure Functions-based -/// implementation. -/// -public static class DicomServerBuilderFunctionClientRegistrationExtensions -{ - /// - /// Adds the necessary services to support the usage of . - /// - /// A service builder for constructing a DICOM server. - /// The root of a configuration containing settings for the client. - /// If service is running in a development environment - /// The service builder for adding additional services. - /// - /// - /// or is . - /// - /// -or- - /// - /// is missing a section with the key TBD - /// - /// - public static IDicomServerBuilder AddAzureFunctionsClient( - this IDicomServerBuilder dicomServerBuilder, - IConfiguration configuration, - bool developmentEnvironment = false) - { - EnsureArg.IsNotNull(dicomServerBuilder, nameof(dicomServerBuilder)); - EnsureArg.IsNotNull(configuration, nameof(configuration)); - - IServiceCollection services = dicomServerBuilder.Services; - services.AddOptions() - .Bind(configuration.GetSection(DicomFunctionOptions.SectionName)) - .ValidateDataAnnotations(); - services.AddDurableClientFactory( - x => configuration - .GetSection(DicomFunctionOptions.SectionName) - .GetSection(nameof(DicomFunctionOptions.DurableTask)) - .Bind(x)); - - services.Configure(o => o.ConfigureDefaultDicomSettings()); - services.Replace(ServiceDescriptor.Singleton()); - services.TryAddScoped(); - - services.AddAzureClientsCore(); - services.TryAddScoped(); - if (!developmentEnvironment) - { - services.AddHealthChecks().AddCheck("DurableTask"); - } - - return dicomServerBuilder; - } -} diff --git a/src/Microsoft.Health.Dicom.Functions.Client/TaskHub/AzureStorageTaskHub.cs b/src/Microsoft.Health.Dicom.Functions.Client/TaskHub/AzureStorageTaskHub.cs deleted file mode 100644 index 281041f80f..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Client/TaskHub/AzureStorageTaskHub.cs +++ /dev/null @@ -1,68 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.Extensions.Logging; - -namespace Microsoft.Health.Dicom.Functions.Client.TaskHub; - -internal class AzureStorageTaskHub : ITaskHub -{ - private readonly ControlQueueCollection _controlQueues; - private readonly WorkItemQueue _workItemQueue; - private readonly InstanceTable _instanceTable; - private readonly HistoryTable _historyTable; - private readonly ILogger _logger; - - public AzureStorageTaskHub( - ControlQueueCollection controlQueues, - WorkItemQueue workItemQueue, - InstanceTable instanceTable, - HistoryTable historyTable, - ILogger logger) - { - _controlQueues = EnsureArg.IsNotNull(controlQueues, nameof(controlQueues)); - _workItemQueue = EnsureArg.IsNotNull(workItemQueue, nameof(workItemQueue)); - _instanceTable = EnsureArg.IsNotNull(instanceTable, nameof(instanceTable)); - _historyTable = EnsureArg.IsNotNull(historyTable, nameof(historyTable)); - _logger = EnsureArg.IsNotNull(logger, nameof(logger)); - } - - public async ValueTask IsHealthyAsync(CancellationToken cancellationToken = default) - { - ValueTask controlQueueTask = _controlQueues.ExistAsync(cancellationToken); - ValueTask workItemQueueTask = _workItemQueue.ExistsAsync(cancellationToken); - ValueTask instanceTableTask = _instanceTable.ExistsAsync(cancellationToken); - ValueTask historyTableTask = _historyTable.ExistsAsync(cancellationToken); - - (bool ControlQueues, bool WorkItemQueue, bool InstanceTable, bool HistoryTable) healthCheck = - ( - await controlQueueTask, - await workItemQueueTask, - await instanceTableTask, - await historyTableTask - ); - - // Check that each of the components found in the Task Hub are available - if (!healthCheck.ControlQueues) - _logger.LogWarning("Cannot find one or more of the control queues: [{ControlQueues}].", string.Join(", ", _controlQueues.Names)); - - if (!healthCheck.WorkItemQueue) - _logger.LogWarning("Cannot find work item queue '{WorkItemQueue}.'", _workItemQueue.Name); - - if (!healthCheck.InstanceTable) - _logger.LogWarning("Cannot find instance table '{InstanceTable}.'", _instanceTable.Name); - - if (!healthCheck.HistoryTable) - _logger.LogWarning("Cannot find history table '{HistoryTable}.'", _historyTable.Name); - - return healthCheck.ControlQueues - && healthCheck.WorkItemQueue - && healthCheck.InstanceTable - && healthCheck.HistoryTable; - } -} diff --git a/src/Microsoft.Health.Dicom.Functions.Client/TaskHub/AzureStorageTaskHubClient.cs b/src/Microsoft.Health.Dicom.Functions.Client/TaskHub/AzureStorageTaskHubClient.cs deleted file mode 100644 index 8111565a13..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Client/TaskHub/AzureStorageTaskHubClient.cs +++ /dev/null @@ -1,75 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using Azure.Data.Tables; -using Azure.Storage.Queues; -using EnsureThat; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Microsoft.Azure.WebJobs.Extensions.DurableTask.Options; -using Microsoft.Extensions.Azure; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Functions.Client.Extensions; - -namespace Microsoft.Health.Dicom.Functions.Client.TaskHub; - -internal class AzureStorageTaskHubClient : ITaskHubClient -{ - private readonly LeasesContainer _leasesContainer; - private readonly QueueServiceClient _queueServiceClient; - private readonly TableServiceClient _tableServiceClient; - private readonly ILoggerFactory _loggerFactory; - private readonly ILogger _logger; - - public AzureStorageTaskHubClient( - AzureComponentFactory factory, - IConnectionInfoResolver connectionInfoProvider, - IOptions options, - ILoggerFactory loggerFactory) - { - EnsureArg.IsNotNull(factory, nameof(factory)); - DurableClientOptions clientOptions = EnsureArg.IsNotNull(options?.Value, nameof(options)); - IConfigurationSection connectionSection = EnsureArg.IsNotNull(connectionInfoProvider, nameof(connectionInfoProvider)).Resolve(clientOptions.ConnectionName); - - _leasesContainer = new LeasesContainer(factory.CreateBlobServiceClient(connectionSection), clientOptions.TaskHub); - _queueServiceClient = factory.CreateQueueServiceClient(connectionSection); - _tableServiceClient = factory.CreateTableServiceClient(connectionSection); - _loggerFactory = EnsureArg.IsNotNull(loggerFactory, nameof(loggerFactory)); - _logger = _loggerFactory.CreateLogger(); - } - - internal AzureStorageTaskHubClient( - LeasesContainer leasesContainer, - QueueServiceClient queueServiceClient, - TableServiceClient tableServiceClient, - ILoggerFactory loggerFactory) - { - _leasesContainer = EnsureArg.IsNotNull(leasesContainer, nameof(leasesContainer)); - _queueServiceClient = EnsureArg.IsNotNull(queueServiceClient, nameof(queueServiceClient)); - _tableServiceClient = EnsureArg.IsNotNull(tableServiceClient, nameof(tableServiceClient)); - _loggerFactory = EnsureArg.IsNotNull(loggerFactory, nameof(loggerFactory)); - _logger = _loggerFactory.CreateLogger(); - } - - public async ValueTask GetTaskHubAsync(CancellationToken cancellationToken = default) - { - TaskHubInfo taskHubInfo = await _leasesContainer.GetTaskHubInfoAsync(cancellationToken); - if (taskHubInfo == null) - { - _logger.LogWarning("Cannot find leases blob container '{LeasesContainer}.'", _leasesContainer.Name); - return null; - } - - return new AzureStorageTaskHub( - new ControlQueueCollection(_queueServiceClient, taskHubInfo), - new WorkItemQueue(_queueServiceClient, taskHubInfo.TaskHubName), - new InstanceTable(_tableServiceClient, taskHubInfo.TaskHubName), - new HistoryTable(_tableServiceClient, taskHubInfo.TaskHubName), - _loggerFactory.CreateLogger()); - } -} diff --git a/src/Microsoft.Health.Dicom.Functions.Client/TaskHub/ControlQueueCollection.cs b/src/Microsoft.Health.Dicom.Functions.Client/TaskHub/ControlQueueCollection.cs deleted file mode 100644 index 2d86445604..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Client/TaskHub/ControlQueueCollection.cs +++ /dev/null @@ -1,47 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Azure; -using Azure.Storage.Queues; -using EnsureThat; - -namespace Microsoft.Health.Dicom.Functions.Client.TaskHub; - -internal class ControlQueueCollection -{ - private readonly QueueServiceClient _queueServiceClient; - private readonly TaskHubInfo _taskHubInfo; - - public ControlQueueCollection(QueueServiceClient queueServiceClient, TaskHubInfo taskHubInfo) - { - _queueServiceClient = EnsureArg.IsNotNull(queueServiceClient, nameof(queueServiceClient)); - _taskHubInfo = EnsureArg.IsNotNull(taskHubInfo, nameof(taskHubInfo)); - } - - public IEnumerable Names => Enumerable - .Range(0, _taskHubInfo.PartitionCount) - .Select(i => GetName(_taskHubInfo.TaskHubName, i)); - - public virtual async ValueTask ExistAsync(CancellationToken cancellationToken = default) - { - // Note: The maximum number of partitions is 16 - Response[] responses = await Task - .WhenAll(Names - .Select(n => _queueServiceClient.GetQueueClient(n).ExistsAsync(cancellationToken))); - - return responses.All(x => x.Value); - } - - // See: https://learn.microsoft.com/en-us/rest/api/storageservices/naming-queues-and-metadata#queue-names - [SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Justification = "Queue names must be lowercase.")] - internal static string GetName(string taskHub, int partition) - => string.Format(CultureInfo.InvariantCulture, "{0}-control-{1:D2}", taskHub?.ToLowerInvariant(), partition); -} diff --git a/src/Microsoft.Health.Dicom.Functions.Client/TaskHub/HistoryTable.cs b/src/Microsoft.Health.Dicom.Functions.Client/TaskHub/HistoryTable.cs deleted file mode 100644 index c6d8c3550c..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Client/TaskHub/HistoryTable.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Azure.Data.Tables; -using EnsureThat; - -namespace Microsoft.Health.Dicom.Functions.Client.TaskHub; - -internal class HistoryTable : TrackingTable -{ - public HistoryTable(TableServiceClient tableServiceClient, string taskHubName) - : base(tableServiceClient, GetName(EnsureArg.IsNotNullOrWhiteSpace(taskHubName, nameof(taskHubName)))) - { } - - internal static string GetName(string taskHubName) - => taskHubName + "History"; -} diff --git a/src/Microsoft.Health.Dicom.Functions.Client/TaskHub/ITaskHub.cs b/src/Microsoft.Health.Dicom.Functions.Client/TaskHub/ITaskHub.cs deleted file mode 100644 index f1aa042de6..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Client/TaskHub/ITaskHub.cs +++ /dev/null @@ -1,31 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.Health.Dicom.Functions.Client.TaskHub; - -/// -/// Represents a Durable Task Framework (DTFx) task hub that manages the state of long-running orchestrations. -/// -public interface ITaskHub -{ - /// - /// Asynchronously checks whether the task hub is up-and-running. - /// - /// - /// The token to monitor for cancellation requests. The default value is . - /// - /// - /// A task that represents the asynchronous operation. The value of the - /// property is if the task hub is healthy; otherwise . - /// - /// - /// The requested cancellation. - /// - ValueTask IsHealthyAsync(CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Functions.Client/TaskHub/ITaskHubClient.cs b/src/Microsoft.Health.Dicom.Functions.Client/TaskHub/ITaskHubClient.cs deleted file mode 100644 index 611e368ff8..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Client/TaskHub/ITaskHubClient.cs +++ /dev/null @@ -1,31 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.Health.Dicom.Functions.Client.TaskHub; - -/// -/// Represents a client for retrieving a resolved based on possibly remote metadata. -/// -public interface ITaskHubClient -{ - /// - /// Asynchronously retrieves an from its storage provider. - /// - /// - /// The token to monitor for cancellation requests. The default value is . - /// - /// - /// A task that represents the asynchronous get operation. The value of the - /// property is the resolved task hub if found; otherwise . - /// - /// - /// The requested cancellation. - /// - ValueTask GetTaskHubAsync(CancellationToken cancellationToken = default); -} diff --git a/src/Microsoft.Health.Dicom.Functions.Client/TaskHub/InstanceTable.cs b/src/Microsoft.Health.Dicom.Functions.Client/TaskHub/InstanceTable.cs deleted file mode 100644 index 37dab7b968..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Client/TaskHub/InstanceTable.cs +++ /dev/null @@ -1,19 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Azure.Data.Tables; -using EnsureThat; - -namespace Microsoft.Health.Dicom.Functions.Client.TaskHub; - -internal class InstanceTable : TrackingTable -{ - public InstanceTable(TableServiceClient tableServiceClient, string taskHubName) - : base(tableServiceClient, GetName(EnsureArg.IsNotNullOrWhiteSpace(taskHubName, nameof(taskHubName)))) - { } - - internal static string GetName(string taskHubName) - => taskHubName + "Instances"; -} diff --git a/src/Microsoft.Health.Dicom.Functions.Client/TaskHub/LeasesContainer.cs b/src/Microsoft.Health.Dicom.Functions.Client/TaskHub/LeasesContainer.cs deleted file mode 100644 index 4dee37c6ff..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Client/TaskHub/LeasesContainer.cs +++ /dev/null @@ -1,49 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Diagnostics.CodeAnalysis; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using Azure; -using Azure.Storage.Blobs; -using Azure.Storage.Blobs.Models; -using EnsureThat; - -namespace Microsoft.Health.Dicom.Functions.Client.TaskHub; - -internal class LeasesContainer -{ - private readonly BlobContainerClient _containerClient; - - internal const string TaskHubBlobName = "taskhub.json"; - - public LeasesContainer(BlobServiceClient blobServiceClient, string taskHubName) - => _containerClient = EnsureArg - .IsNotNull(blobServiceClient, nameof(blobServiceClient)) - .GetBlobContainerClient(GetName(taskHubName)); - - public string Name => _containerClient.Name; - - public virtual async ValueTask GetTaskHubInfoAsync(CancellationToken cancellationToken = default) - { - BlobClient client = _containerClient.GetBlobClient(TaskHubBlobName); - - try - { - BlobDownloadResult result = await client.DownloadContentAsync(cancellationToken); - return result.Content.ToObjectFromJson(); - } - catch (RequestFailedException rfe) when (rfe.Status == (int)HttpStatusCode.NotFound) - { - return null; - } - } - - // See: https://learn.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata#container-names - [SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Justification = "Blob container names must be lowercase.")] - internal static string GetName(string taskHub) - => taskHub?.ToLowerInvariant() + "-leases"; -} diff --git a/src/Microsoft.Health.Dicom.Functions.Client/TaskHub/TaskHubInfo.cs b/src/Microsoft.Health.Dicom.Functions.Client/TaskHub/TaskHubInfo.cs deleted file mode 100644 index 28f1e3c064..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Client/TaskHub/TaskHubInfo.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; - -namespace Microsoft.Health.Dicom.Functions.Client.TaskHub; - -internal sealed class TaskHubInfo -{ - public string TaskHubName { get; set; } - - public DateTime CreatedAt { get; set; } - - public int PartitionCount { get; set; } -} diff --git a/src/Microsoft.Health.Dicom.Functions.Client/TaskHub/TrackingTable.cs b/src/Microsoft.Health.Dicom.Functions.Client/TaskHub/TrackingTable.cs deleted file mode 100644 index f762fd9304..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Client/TaskHub/TrackingTable.cs +++ /dev/null @@ -1,43 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using Azure; -using Azure.Data.Tables; -using EnsureThat; - -namespace Microsoft.Health.Dicom.Functions.Client.TaskHub; - -internal abstract class TrackingTable -{ - private readonly TableClient _tableClient; - - protected TrackingTable(TableServiceClient queueServiceClient, string tableName) - => _tableClient = EnsureArg.IsNotNull(queueServiceClient, nameof(queueServiceClient)).GetTableClient(tableName); - - public string Name => _tableClient.Name; - - public virtual async ValueTask ExistsAsync(CancellationToken cancellationToken = default) - { - // Note: There is no ExistsAsync method for TableClient, so instead - // we'll run a query that will (probably) not return any results - AsyncPageable pageable = _tableClient.QueryAsync( - filter: "PartitionKey eq ''", - maxPerPage: 1, - cancellationToken: cancellationToken); - - try - { - await pageable.GetAsyncEnumerator(cancellationToken).MoveNextAsync(); - return true; - } - catch (RequestFailedException rfe) when (rfe.Status == (int)HttpStatusCode.NotFound) - { - return false; - } - } -} diff --git a/src/Microsoft.Health.Dicom.Functions.Client/TaskHub/WorkItemQueue.cs b/src/Microsoft.Health.Dicom.Functions.Client/TaskHub/WorkItemQueue.cs deleted file mode 100644 index ea21d62900..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.Client/TaskHub/WorkItemQueue.cs +++ /dev/null @@ -1,32 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Diagnostics.CodeAnalysis; -using System.Threading; -using System.Threading.Tasks; -using Azure.Storage.Queues; -using EnsureThat; - -namespace Microsoft.Health.Dicom.Functions.Client.TaskHub; - -internal class WorkItemQueue -{ - private readonly QueueClient _queueClient; - - public WorkItemQueue(QueueServiceClient queueServiceClient, string taskHubName) - => _queueClient = EnsureArg - .IsNotNull(queueServiceClient, nameof(queueServiceClient)) - .GetQueueClient(GetName(EnsureArg.IsNotNullOrWhiteSpace(taskHubName, nameof(taskHubName)))); - - public string Name => _queueClient.Name; - - public virtual async ValueTask ExistsAsync(CancellationToken cancellationToken = default) - => await _queueClient.ExistsAsync(cancellationToken); - - // See: https://learn.microsoft.com/en-us/rest/api/storageservices/naming-queues-and-metadata#queue-names - [SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Justification = "Queue names must be lowercase.")] - internal static string GetName(string taskHub) - => taskHub?.ToLowerInvariant() + "-workitems"; -} diff --git a/src/Microsoft.Health.Dicom.Functions.UnitTests/ContentLengthBackFill/ContentLengthBackFillDurableFunctionTests.Activity.cs b/src/Microsoft.Health.Dicom.Functions.UnitTests/ContentLengthBackFill/ContentLengthBackFillDurableFunctionTests.Activity.cs deleted file mode 100644 index ef21f2ef14..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.UnitTests/ContentLengthBackFill/ContentLengthBackFillDurableFunctionTests.Activity.cs +++ /dev/null @@ -1,287 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Azure; -using Azure.Storage.Blobs.Models; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Functions.ContentLengthBackFill; -using Microsoft.Health.Dicom.Functions.ContentLengthBackFill.Models; -using Microsoft.Health.Dicom.Tests.Common; -using Microsoft.IdentityModel.Tokens; -using NSubstitute; -using NSubstitute.ExceptionExtensions; -using Xunit; - -namespace Microsoft.Health.Dicom.Functions.UnitTests.ContentLengthBackFill; - -public partial class ContentLengthBackFillDurableFunctionTests -{ - [Fact] - public async Task GivenBatch_WhenGettingInstanceBatches_ThenShouldInvokeCorrectMethodWithCorrectParams() - { - const int batchSize = 100; - const int maxParallelBatches = 3; - - IReadOnlyList expected = new List { new WatermarkRange(12345, 678910) }; - _instanceStore - .GetContentLengthBackFillInstanceBatches(batchSize, maxParallelBatches, CancellationToken.None) - .Returns(expected); - - IReadOnlyList actual = await _contentLengthBackFillDurableFunction.GetContentLengthBackFillInstanceBatches( - new BatchCreationArguments(batchSize, maxParallelBatches), - NullLogger.Instance); - - Assert.Same(expected, actual); - await _instanceStore - .Received(1) - .GetContentLengthBackFillInstanceBatches(batchSize, maxParallelBatches, CancellationToken.None); - } - - [Fact] - public async Task GivenBatch_WhenBackFillContentLengthRangeDataAsync_ThenShouldBackfillEachInstance() - { - var watermarkRange = new WatermarkRange(3, 10); - - var expected = new List - { - new(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 3), - new(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 4), - new(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 5), - new(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 6), - new(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 7), - new(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 8), - new(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 9), - }; - - // Arrange input - _instanceStore - .GetContentLengthBackFillInstanceIdentifiersByWatermarkRangeAsync(watermarkRange, Arg.Any()) - .Returns(expected); - - var expectedFileProperty = new FileProperties { ContentLength = 123, Path = "new path", ETag = "new etag" }; - foreach (VersionedInstanceIdentifier identifier in expected) - { - _fileStore.GetFilePropertiesAsync( - identifier.Version, - identifier.Partition, - fileProperties: null, - Arg.Any()) - .Returns(expectedFileProperty); - } - - Dictionary expectedFilePropertiesByWatermark = new Dictionary(); - foreach (var x in expected) - { - expectedFilePropertiesByWatermark.TryAdd(x.Version, expectedFileProperty); - } - - _indexStore.UpdateFilePropertiesContentLengthAsync(expectedFilePropertiesByWatermark).Returns(Task.CompletedTask); - - // Call the activity - await _contentLengthBackFillDurableFunction.BackFillContentLengthRangeDataAsync(watermarkRange, NullLogger.Instance); - - // Assert behavior - await _instanceStore - .Received(1) - .GetContentLengthBackFillInstanceIdentifiersByWatermarkRangeAsync(watermarkRange, CancellationToken.None); - - foreach (VersionedInstanceIdentifier identifier in expected) - { - await _fileStore - .Received(1) - .GetFilePropertiesAsync(identifier.Version, identifier.Partition, fileProperties: null, - Arg.Any()); - } - - await _indexStore.Received(1).UpdateFilePropertiesContentLengthAsync(Arg.Is>(x => - x.Keys.SequenceEqual(expected.Select(y => y.Version)) && - x.Values.All(y => y.ContentLength == expectedFileProperty.ContentLength))); - } - - [Theory] - [InlineData(0)] - [InlineData(-1)] - public async Task GivenBatch_WhenInstanceBlobLengthIsLessThanOne_ThenExpectInstanceUpdatedWithNegativeOne(long corruptedContentLength) - { - var watermarkRange = new WatermarkRange(3, 10); - - var expected = new List - { - new(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 3), - new(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 4), - new(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 5) - }; - - // Arrange input - _instanceStore - .GetContentLengthBackFillInstanceIdentifiersByWatermarkRangeAsync(watermarkRange, Arg.Any()) - .Returns(expected); - - var expectedFileProperty = new FileProperties { ContentLength = 123, Path = "new path", ETag = "new etag" }; - - // we will sandwich the corrupted data in between good data - _fileStore.GetFilePropertiesAsync(expected[0].Version, expected[0].Partition, fileProperties: null, Arg.Any()) - .Returns(expectedFileProperty); - - _fileStore.GetFilePropertiesAsync(expected[1].Version, expected[1].Partition, fileProperties: null, Arg.Any()) - .Returns(new FileProperties { ContentLength = corruptedContentLength, Path = "new path", ETag = "new etag" }); - - _fileStore.GetFilePropertiesAsync(expected[2].Version, expected[2].Partition, fileProperties: null, Arg.Any()) - .Returns(expectedFileProperty); - - IReadOnlyDictionary expectedFilePropertiesByWatermark = new Dictionary() - { - [expected[0].Version] = expectedFileProperty, - [expected[1].Version] = new() { ContentLength = ContentLengthBackFillDurableFunction.CorruptedAndProcessed }, - [expected[2].Version] = expectedFileProperty - }; - - _indexStore.UpdateFilePropertiesContentLengthAsync(expectedFilePropertiesByWatermark).Returns(Task.CompletedTask); - - // Call the activity - await _contentLengthBackFillDurableFunction.BackFillContentLengthRangeDataAsync(watermarkRange, NullLogger.Instance); - - // Assert behavior - await _instanceStore - .Received(1) - .GetContentLengthBackFillInstanceIdentifiersByWatermarkRangeAsync(watermarkRange, CancellationToken.None); - - foreach (VersionedInstanceIdentifier identifier in expected) - { - await _fileStore - .Received(1) - .GetFilePropertiesAsync(identifier.Version, identifier.Partition, fileProperties: null, Arg.Any()); - } - - await _indexStore.Received(1).UpdateFilePropertiesContentLengthAsync(Arg.Is>(x => - x.Keys.SequenceEqual(expectedFilePropertiesByWatermark.Keys) && - x.Values.Select(fp => fp.ContentLength).SequenceEqual(expectedFilePropertiesByWatermark.Values.Select(fileProperty => fileProperty.ContentLength)))); - } - - public static IEnumerable GetEtagExceptions() - { - yield return new object[] - { - new DataStoreRequestFailedException(new RequestFailedException( - status: 412, - message: string.Empty, - errorCode: BlobErrorCode.ConditionNotMet.ToString(), - innerException: new Exception()), - isExternal: true) - }; - } - - public static IEnumerable GetExceptions() - { - yield return new object[] - { - new DataStoreRequestFailedException(new RequestFailedException( - status: 412, - message: string.Empty, - errorCode: BlobErrorCode.AuthenticationFailed.ToString(), - innerException: new Exception()), - isExternal: true) - }; - } - - [Theory] - [MemberData(nameof(GetEtagExceptions))] - public async Task GivenBatch_WhenExceptionOnGettingLengthFromBlobStoreDueToEtagMismatch_ThenExpectInstanceUpdatedWithNegativeOne(Exception exception) - { - var watermarkRange = new WatermarkRange(3, 10); - - var expected = new List - { - new(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 3), - }; - - // Arrange input - _instanceStore - .GetContentLengthBackFillInstanceIdentifiersByWatermarkRangeAsync(watermarkRange, Arg.Any()) - .Returns(expected); - - _fileStore.GetFilePropertiesAsync(Arg.Any(), expected[0].Partition, fileProperties: null, Arg.Any()) - .Throws(exception); - - var expectedFileProperty = new FileProperties { ContentLength = ContentLengthBackFillDurableFunction.CorruptedAndProcessed }; - - IReadOnlyDictionary expectedFilePropertiesByWatermark = new Dictionary() - { - [expected[0].Version] = expectedFileProperty - }; - - _indexStore.UpdateFilePropertiesContentLengthAsync(expectedFilePropertiesByWatermark).Returns(Task.CompletedTask); - - // Call the activity - await _contentLengthBackFillDurableFunction.BackFillContentLengthRangeDataAsync(watermarkRange, NullLogger.Instance); - - // Assert behavior - await _instanceStore - .Received(1) - .GetContentLengthBackFillInstanceIdentifiersByWatermarkRangeAsync(watermarkRange, CancellationToken.None); - - foreach (VersionedInstanceIdentifier identifier in expected) - { - await _fileStore - .Received(1) - .GetFilePropertiesAsync(identifier.Version, identifier.Partition, fileProperties: null, Arg.Any()); - } - - await _indexStore.Received(1).UpdateFilePropertiesContentLengthAsync(Arg.Is>(x => - x.Keys.SequenceEqual(expectedFilePropertiesByWatermark.Keys) && - x.Values.Select(fp => fp.ContentLength).SequenceEqual(expectedFilePropertiesByWatermark.Values.Select(fileProperty => fileProperty.ContentLength)))); - } - - - [Theory] - [MemberData(nameof(GetExceptions))] - public async Task GivenBatch_WhenExceptionOnGettingLengthFromBlobStoreNotDueToEtag_ThenExpectInstanceNotUpdated(Exception exception) - { - var watermarkRange = new WatermarkRange(3, 10); - - var expected = new List - { - new(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 3), - }; - - // Arrange input - _instanceStore - .GetContentLengthBackFillInstanceIdentifiersByWatermarkRangeAsync(watermarkRange, Arg.Any()) - .Returns(expected); - - _fileStore.GetFilePropertiesAsync(Arg.Any(), expected[0].Partition, fileProperties: null, Arg.Any()) - .Throws(exception); - - IReadOnlyDictionary expectedFilePropertiesByWatermark = new Dictionary(); - _indexStore.UpdateFilePropertiesContentLengthAsync(expectedFilePropertiesByWatermark).Returns(Task.CompletedTask); - - // Call the activity - await _contentLengthBackFillDurableFunction.BackFillContentLengthRangeDataAsync(watermarkRange, NullLogger.Instance); - - // Assert behavior - await _instanceStore - .Received(1) - .GetContentLengthBackFillInstanceIdentifiersByWatermarkRangeAsync(watermarkRange, CancellationToken.None); - - foreach (VersionedInstanceIdentifier identifier in expected) - { - await _fileStore - .Received(1) - .GetFilePropertiesAsync(identifier.Version, identifier.Partition, fileProperties: null, Arg.Any()); - } - - await _indexStore.Received(1).UpdateFilePropertiesContentLengthAsync(Arg.Is>(x => - x.Values.IsNullOrEmpty() && - x.Values.SequenceEqual(expectedFilePropertiesByWatermark.Values))); - } -} diff --git a/src/Microsoft.Health.Dicom.Functions.UnitTests/ContentLengthBackFill/ContentLengthBackFillDurableFunctionTests.Orchestration.cs b/src/Microsoft.Health.Dicom.Functions.UnitTests/ContentLengthBackFill/ContentLengthBackFillDurableFunctionTests.Orchestration.cs deleted file mode 100644 index 0c4441fc3c..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.UnitTests/ContentLengthBackFill/ContentLengthBackFillDurableFunctionTests.Orchestration.cs +++ /dev/null @@ -1,191 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Functions.ContentLengthBackFill; -using Microsoft.Health.Dicom.Functions.ContentLengthBackFill.Models; -using Microsoft.Health.Operations; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Functions.UnitTests.ContentLengthBackFill; -public partial class ContentLengthBackFillDurableFunctionTests -{ - [Fact] - public async Task GivenNewOrchestrationWithInput_WhenBackFilling_ThenDivideAndCleanupBatches() - { - const int batchSize = 5; - const int maxParallelBatches = 3; - - DateTime createdTime = DateTime.UtcNow; - - var batching = new BatchingOptions - { - MaxParallelCount = maxParallelBatches, - Size = batchSize, - }; - - IReadOnlyList expectedBatches = CreateBatches(50, batchSize: batchSize, maxParallelBatches: maxParallelBatches); - var expectedInput = new ContentLengthBackFillCheckPoint - { - Batching = batching, - CreatedTime = createdTime - }; - - // Arrange the input - IDurableOrchestrationContext context = CreateContext(OperationId.Generate()); - - context - .GetInput() - .Returns(expectedInput); - - context - .CallActivityWithRetryAsync>( - nameof(ContentLengthBackFillDurableFunction.GetContentLengthBackFillInstanceBatches), - _options.RetryOptions, - Arg.Is( - x => x.BatchSize == batchSize && x.MaxParallelBatches == maxParallelBatches)) - .Returns(expectedBatches); - - context - .CallActivityWithRetryAsync( - nameof(ContentLengthBackFillDurableFunction.BackFillContentLengthRangeDataAsync), - _options.RetryOptions, - Arg.Any()) - .Returns(Task.CompletedTask); - - // Invoke the orchestration - await _contentLengthBackFillDurableFunction.ContentLengthBackFillAsync(context, NullLogger.Instance); - - // Assert behavior - context - .Received(1) - .GetInput(); - - await context - .Received(1) - .CallActivityWithRetryAsync>( - nameof(ContentLengthBackFillDurableFunction.GetContentLengthBackFillInstanceBatches), - _options.RetryOptions, - Arg.Is( - x => x.BatchSize == batchSize && x.MaxParallelBatches == maxParallelBatches)); - - await context - .Received(3) - .CallActivityWithRetryAsync( - nameof(ContentLengthBackFillDurableFunction.BackFillContentLengthRangeDataAsync), - _options.RetryOptions, - Arg.Any()); - - context - .Received(1) - .ContinueAsNew( - Arg.Is(x => - x.Batching == batching && - x.CreatedTime == expectedInput.CreatedTime)); - } - - [Fact] - public async Task GivenNewOrchestrationWithInput_WhenBackFillingButNoBatchesAvailable_ThenExpectUpdateMethodsNotCalled() - { - const int batchSize = 5; - const int maxParallelBatches = 3; - - DateTime createdTime = DateTime.UtcNow; - - var batching = new BatchingOptions - { - MaxParallelCount = maxParallelBatches, - Size = batchSize, - }; - - IReadOnlyList expectedBatches = CreateBatches(0, batchSize: batchSize, maxParallelBatches: maxParallelBatches); - Assert.Empty(expectedBatches); - - var expectedInput = new ContentLengthBackFillCheckPoint - { - Batching = batching, - CreatedTime = createdTime - }; - - // Arrange the input - IDurableOrchestrationContext context = CreateContext(OperationId.Generate()); - - context - .GetInput() - .Returns(expectedInput); - - context - .CallActivityWithRetryAsync>( - nameof(ContentLengthBackFillDurableFunction.GetContentLengthBackFillInstanceBatches), - _options.RetryOptions, - Arg.Is( - x => x.BatchSize == batchSize && x.MaxParallelBatches == maxParallelBatches)) - .Returns(expectedBatches); - - context - .CallActivityWithRetryAsync( - nameof(ContentLengthBackFillDurableFunction.BackFillContentLengthRangeDataAsync), - _options.RetryOptions, - Arg.Any()) - .Returns(Task.CompletedTask); - - // Invoke the orchestration - await _contentLengthBackFillDurableFunction.ContentLengthBackFillAsync(context, NullLogger.Instance); - - // Assert behavior - context - .Received(1) - .GetInput(); - - await context - .Received(1) - .CallActivityWithRetryAsync>( - nameof(ContentLengthBackFillDurableFunction.GetContentLengthBackFillInstanceBatches), - _options.RetryOptions, - Arg.Is( - x => x.BatchSize == batchSize && x.MaxParallelBatches == maxParallelBatches)); - - await context - .DidNotReceive() - .CallActivityWithRetryAsync( - nameof(ContentLengthBackFillDurableFunction.BackFillContentLengthRangeDataAsync), - _options.RetryOptions, - Arg.Any()); - - context - .DidNotReceive() - .ContinueAsNew( - Arg.Is(x => - x.Batching == batching), - false); - } - - private static IDurableOrchestrationContext CreateContext(string operationId) - { - IDurableOrchestrationContext context = Substitute.For(); - context.InstanceId.Returns(operationId); - return context; - } - - private static IReadOnlyList CreateBatches(long end, int batchSize, int maxParallelBatches) - { - var batches = new List(); - - long current = end; - for (int i = 0; i < maxParallelBatches && current > 0; i++) - { - batches.Add(new WatermarkRange(Math.Max(1, current - batchSize + 1), current)); - current -= batchSize; - } - - return batches; - } -} diff --git a/src/Microsoft.Health.Dicom.Functions.UnitTests/ContentLengthBackFill/ContentLengthBackFillDurableFunctionTests.cs b/src/Microsoft.Health.Dicom.Functions.UnitTests/ContentLengthBackFill/ContentLengthBackFillDurableFunctionTests.cs deleted file mode 100644 index 164e14204a..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.UnitTests/ContentLengthBackFill/ContentLengthBackFillDurableFunctionTests.cs +++ /dev/null @@ -1,36 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Retrieve; -using Microsoft.Health.Dicom.Core.Features.Store; -using Microsoft.Health.Dicom.Functions.ContentLengthBackFill; -using Microsoft.Health.Operations.Functions.DurableTask; -using NSubstitute; - -namespace Microsoft.Health.Dicom.Functions.UnitTests.ContentLengthBackFill; - -public partial class ContentLengthBackFillDurableFunctionTests -{ - private readonly ContentLengthBackFillDurableFunction _contentLengthBackFillDurableFunction; - private readonly IInstanceStore _instanceStore; - private readonly IIndexDataStore _indexStore; - private readonly IFileStore _fileStore; - private readonly ContentLengthBackFillOptions _options; - - public ContentLengthBackFillDurableFunctionTests() - { - _instanceStore = Substitute.For(); - _indexStore = Substitute.For(); - _fileStore = Substitute.For(); - _options = new ContentLengthBackFillOptions { RetryOptions = new ActivityRetryOptions() }; - _contentLengthBackFillDurableFunction = new ContentLengthBackFillDurableFunction( - _instanceStore, - _indexStore, - _fileStore, - Options.Create(_options)); - } -} diff --git a/src/Microsoft.Health.Dicom.Functions.UnitTests/DataCleanup/DataCleanupDurableFunctionTests.Activity.cs b/src/Microsoft.Health.Dicom.Functions.UnitTests/DataCleanup/DataCleanupDurableFunctionTests.Activity.cs deleted file mode 100644 index 4140c640b1..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.UnitTests/DataCleanup/DataCleanupDurableFunctionTests.Activity.cs +++ /dev/null @@ -1,168 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Models; -using Microsoft.Health.Dicom.Functions.DataCleanup.Models; -using Microsoft.Health.Dicom.Tests.Common; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Functions.UnitTests.DataCleanup; - -public partial class DataCleanupDurableFunctionTests -{ - [Fact] - public async Task GivenNoWatermark_WhenGettingInstanceBatches_ThenShouldInvokeCorrectMethod() - { - const int batchSize = 100; - const int maxParallelBatches = 3; - var now = DateTime.UtcNow; - var startTimeStamp = now; - var endTimeStamp = now.AddDays(1); - - IReadOnlyList expected = new List { new WatermarkRange(12345, 678910) }; - _instanceStore - .GetInstanceBatchesByTimeStampAsync(batchSize, maxParallelBatches, IndexStatus.Created, startTimeStamp, endTimeStamp, null, CancellationToken.None) - .Returns(expected); - - IReadOnlyList actual = await _dataCleanupDurableFunction.GetInstanceBatchesByTimeStampAsync( - new DataCleanupBatchCreationArguments(null, batchSize, maxParallelBatches, startTimeStamp, endTimeStamp), - NullLogger.Instance); - - Assert.Same(expected, actual); - await _instanceStore - .Received(1) - .GetInstanceBatchesByTimeStampAsync(batchSize, maxParallelBatches, IndexStatus.Created, startTimeStamp, endTimeStamp, null, CancellationToken.None); - } - - [Theory] - [InlineData(0)] - [InlineData(1)] - [InlineData(10)] - public async Task GivenWatermark_WhenGettingInstanceBatches_ThenShouldInvokeCorrectMethod(long max) - { - const int batchSize = 100; - const int maxParallelBatches = 3; - var now = DateTime.UtcNow; - var startTimeStamp = now; - var endTimeStamp = now.AddDays(1); - - IReadOnlyList expected = new List { new WatermarkRange(1, 2) }; // watermarks don't matter - _instanceStore - .GetInstanceBatchesByTimeStampAsync(batchSize, maxParallelBatches, IndexStatus.Created, startTimeStamp, endTimeStamp, max, CancellationToken.None) - .Returns(expected); - - IReadOnlyList actual = await _dataCleanupDurableFunction.GetInstanceBatchesByTimeStampAsync( - new DataCleanupBatchCreationArguments(max, batchSize, maxParallelBatches, startTimeStamp, endTimeStamp), - NullLogger.Instance); - - Assert.Same(expected, actual); - await _instanceStore - .Received(1) - .GetInstanceBatchesByTimeStampAsync(batchSize, maxParallelBatches, IndexStatus.Created, startTimeStamp, endTimeStamp, max, CancellationToken.None); - } - - [Fact] - public async Task GivenBatch_WhenCleanup_ThenShouldMigrateEachInstance() - { - var args = new WatermarkRange(3, 10); - - var expected = new List - { - new VersionedInstanceIdentifier(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 3), - new VersionedInstanceIdentifier(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 4), - new VersionedInstanceIdentifier(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 5), - new VersionedInstanceIdentifier(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 6), - new VersionedInstanceIdentifier(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 7), - new VersionedInstanceIdentifier(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 8), - new VersionedInstanceIdentifier(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 9), - }; - - // Arrange input - _instanceStore - .GetInstanceIdentifiersByWatermarkRangeAsync(args, IndexStatus.Created, Arg.Any()) - .Returns(expected); - - foreach (VersionedInstanceIdentifier identifier in expected) - { - _metadataStore.DoesFrameRangeExistAsync(identifier.Version, Arg.Any()).Returns(true); - } - - var versions = expected.Select(x => x.Version).ToList(); - - _indexStore.UpdateFrameDataAsync(expected.First().Partition.Key, versions, true, Arg.Any()).Returns(Task.CompletedTask); - - // Call the activity - await _dataCleanupDurableFunction.CleanupFrameRangeDataAsync(args, NullLogger.Instance); - - // Assert behavior - await _instanceStore - .Received(1) - .GetInstanceIdentifiersByWatermarkRangeAsync(args, IndexStatus.Created, CancellationToken.None); - - foreach (VersionedInstanceIdentifier identifier in expected) - { - await _metadataStore.Received(1).DoesFrameRangeExistAsync(identifier.Version, Arg.Any()); - } - - await _indexStore.Received(1).UpdateFrameDataAsync(expected.First().Partition.Key, Arg.Is>(x => versions.Intersect(x).Count() == versions.Count()), true, Arg.Any()); - } - - [Fact] - public async Task GivenBatchInDifferentPartition_WhenCleanup_ThenShouldMigrateEachInstance() - { - var args = new WatermarkRange(3, 10); - - var partition = new Core.Features.Partitioning.Partition(2, "New Partition"); - - var expected = new List - { - new VersionedInstanceIdentifier(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 3), - new VersionedInstanceIdentifier(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 4), - new VersionedInstanceIdentifier(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 5), - new VersionedInstanceIdentifier(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 6), - new VersionedInstanceIdentifier(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 7), - new VersionedInstanceIdentifier(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 8, partition), - new VersionedInstanceIdentifier(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 9, partition) - }; - - // Arrange input - _instanceStore - .GetInstanceIdentifiersByWatermarkRangeAsync(args, IndexStatus.Created, Arg.Any()) - .Returns(expected); - - foreach (VersionedInstanceIdentifier identifier in expected) - { - _metadataStore.DoesFrameRangeExistAsync(identifier.Version, Arg.Any()).Returns(true); - } - - _indexStore.UpdateFrameDataAsync(Arg.Any(), expected.Select(x => x.Version).ToList(), true, Arg.Any()).Returns(Task.CompletedTask); - - // Call the activity - await _dataCleanupDurableFunction.CleanupFrameRangeDataAsync(args, NullLogger.Instance); - - // Assert behavior - await _instanceStore - .Received(1) - .GetInstanceIdentifiersByWatermarkRangeAsync(args, IndexStatus.Created, CancellationToken.None); - - foreach (VersionedInstanceIdentifier identifier in expected) - { - await _metadataStore.Received(1).DoesFrameRangeExistAsync(identifier.Version, Arg.Any()); - } - - var expectedVersions1 = expected.Where(x => x.Partition.Key == 1).Select(x => x.Version).OrderBy(x => x).ToList(); - var expectedVersions2 = expected.Where(x => x.Partition.Key == 2).Select(x => x.Version).OrderBy(x => x).ToList(); - await _indexStore.Received(1).UpdateFrameDataAsync(expected.First().Partition.Key, Arg.Is>(x => expectedVersions1.Intersect(x).Count() == expectedVersions1.Count()), true, Arg.Any()); - await _indexStore.Received(1).UpdateFrameDataAsync(partition.Key, Arg.Is>(x => expectedVersions2.Intersect(x).Count() == expectedVersions2.Count()), true, Arg.Any()); - } -} diff --git a/src/Microsoft.Health.Dicom.Functions.UnitTests/DataCleanup/DataCleanupDurableFunctionTests.Orchestration.cs b/src/Microsoft.Health.Dicom.Functions.UnitTests/DataCleanup/DataCleanupDurableFunctionTests.Orchestration.cs deleted file mode 100644 index 1a9e8516e9..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.UnitTests/DataCleanup/DataCleanupDurableFunctionTests.Orchestration.cs +++ /dev/null @@ -1,189 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Functions.DataCleanup; -using Microsoft.Health.Dicom.Functions.DataCleanup.Models; -using Microsoft.Health.Operations; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Functions.UnitTests.DataCleanup; -public partial class DataCleanupDurableFunctionTests -{ - [Fact] - public async Task GivenNewOrchestrationWithInput_WhenCleanupInstances_ThenDivideAndCleanupBatches() - { - const int batchSize = 5; - const int maxParallelBatches = 3; - - var now = DateTime.UtcNow; - var startTimeStamp = now; - var endTimeStamp = now.AddDays(1); - - DateTime createdTime = DateTime.UtcNow; - - var batching = new BatchingOptions - { - MaxParallelCount = maxParallelBatches, - Size = batchSize, - }; - - IReadOnlyList expectedBatches = CreateBatches(50); - var expectedInput = new DataCleanupCheckPoint - { - Batching = batching, - StartFilterTimeStamp = startTimeStamp, - EndFilterTimeStamp = endTimeStamp, - CreatedTime = createdTime - }; - - // Arrange the input - string operationId = OperationId.Generate(); - IDurableOrchestrationContext context = CreateContext(operationId); - context - .GetInput() - .Returns(expectedInput); - context - .CallActivityWithRetryAsync>( - nameof(DataCleanupDurableFunction.GetInstanceBatchesByTimeStampAsync), - _options.RetryOptions, - Arg.Is(x => x.StartFilterTimeStamp == now && x.EndFilterTimeStamp == now.AddDays(1))) - .Returns(expectedBatches); - context - .CallActivityWithRetryAsync( - nameof(DataCleanupDurableFunction.CleanupFrameRangeDataAsync), - _options.RetryOptions, - Arg.Any()) - .Returns(Task.CompletedTask); - - // Invoke the orchestration - await _dataCleanupDurableFunction.DataCleanupAsync(context, NullLogger.Instance); - - // Assert behavior - context - .Received(1) - .GetInput(); - await context - .Received(1) - .CallActivityWithRetryAsync>( - nameof(DataCleanupDurableFunction.GetInstanceBatchesByTimeStampAsync), - _options.RetryOptions, - Arg.Is(x => x.StartFilterTimeStamp == now && x.EndFilterTimeStamp == now.AddDays(1))); - - await context - .Received(3) - .CallActivityWithRetryAsync( - nameof(DataCleanupDurableFunction.CleanupFrameRangeDataAsync), - _options.RetryOptions, - Arg.Any()); - - context - .Received(1) - .ContinueAsNew( - Arg.Is(x => x.StartFilterTimeStamp == now && x.EndFilterTimeStamp == now.AddDays(1)), - false); - } - - [Fact] - public async Task GivenNewOrchestrationWithNoBatches_WhenCleanupInstances_ThenDivideAndCleanupBatches() - { - const int batchSize = 5; - const int maxParallelBatches = 3; - - var now = DateTime.UtcNow; - var startTimeStamp = now; - var endTimeStamp = now.AddDays(1); - - DateTime createdTime = DateTime.UtcNow; - - var batching = new BatchingOptions - { - MaxParallelCount = maxParallelBatches, - Size = batchSize, - }; - - IReadOnlyList expectedBatches = CreateBatches(0); - var expectedInput = new DataCleanupCheckPoint - { - Batching = batching, - StartFilterTimeStamp = startTimeStamp, - EndFilterTimeStamp = endTimeStamp, - CreatedTime = createdTime - }; - - // Arrange the input - string operationId = OperationId.Generate(); - IDurableOrchestrationContext context = CreateContext(operationId); - context - .GetInput() - .Returns(expectedInput); - context - .CallActivityWithRetryAsync>( - nameof(DataCleanupDurableFunction.GetInstanceBatchesByTimeStampAsync), - _options.RetryOptions, - Arg.Is(x => x.StartFilterTimeStamp == now && x.EndFilterTimeStamp == now.AddDays(1))) - .Returns(expectedBatches); - context - .CallActivityWithRetryAsync( - nameof(DataCleanupDurableFunction.CleanupFrameRangeDataAsync), - _options.RetryOptions, - Arg.Any()) - .Returns(Task.CompletedTask); - - // Invoke the orchestration - await _dataCleanupDurableFunction.DataCleanupAsync(context, NullLogger.Instance); - - // Assert behavior - context - .Received(1) - .GetInput(); - await context - .Received(1) - .CallActivityWithRetryAsync>( - nameof(DataCleanupDurableFunction.GetInstanceBatchesByTimeStampAsync), - _options.RetryOptions, - Arg.Is(x => x.StartFilterTimeStamp == now && x.EndFilterTimeStamp == now.AddDays(1))); - - await context - .DidNotReceive() - .CallActivityWithRetryAsync( - nameof(DataCleanupDurableFunction.CleanupFrameRangeDataAsync), - _options.RetryOptions, - Arg.Any()); - - context - .DidNotReceive() - .ContinueAsNew( - Arg.Is(x => x.StartFilterTimeStamp == now && x.EndFilterTimeStamp == now.AddDays(1)), - false); - } - - private static IDurableOrchestrationContext CreateContext(string operationId) - { - IDurableOrchestrationContext context = Substitute.For(); - context.InstanceId.Returns(operationId); - return context; - } - - private static IReadOnlyList CreateBatches(long end, int batchSize = 5, int maxParallelBatches = 3) - { - var batches = new List(); - - long current = end; - for (int i = 0; i < maxParallelBatches && current > 0; i++) - { - batches.Add(new WatermarkRange(Math.Max(1, current - batchSize + 1), current)); - current -= batchSize; - } - - return batches; - } -} diff --git a/src/Microsoft.Health.Dicom.Functions.UnitTests/DataCleanup/DataCleanupDurableFunctionTests.cs b/src/Microsoft.Health.Dicom.Functions.UnitTests/DataCleanup/DataCleanupDurableFunctionTests.cs deleted file mode 100644 index dcaf5e52fc..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.UnitTests/DataCleanup/DataCleanupDurableFunctionTests.cs +++ /dev/null @@ -1,36 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Retrieve; -using Microsoft.Health.Dicom.Core.Features.Store; -using Microsoft.Health.Dicom.Functions.DataCleanup; -using Microsoft.Health.Operations.Functions.DurableTask; -using NSubstitute; - -namespace Microsoft.Health.Dicom.Functions.UnitTests.DataCleanup; - -public partial class DataCleanupDurableFunctionTests -{ - private readonly DataCleanupDurableFunction _dataCleanupDurableFunction; - private readonly IInstanceStore _instanceStore; - private readonly IIndexDataStore _indexStore; - private readonly IMetadataStore _metadataStore; - private readonly DataCleanupOptions _options; - - public DataCleanupDurableFunctionTests() - { - _instanceStore = Substitute.For(); - _indexStore = Substitute.For(); - _metadataStore = Substitute.For(); - _options = new DataCleanupOptions { RetryOptions = new ActivityRetryOptions() }; - _dataCleanupDurableFunction = new DataCleanupDurableFunction( - _instanceStore, - _indexStore, - _metadataStore, - Options.Create(_options)); - } -} diff --git a/src/Microsoft.Health.Dicom.Functions.UnitTests/Export/ExportDurableFunctionTests.Activity.cs b/src/Microsoft.Health.Dicom.Functions.UnitTests/Export/ExportDurableFunctionTests.Activity.cs deleted file mode 100644 index 4ae95037ac..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.UnitTests/Export/ExportDurableFunctionTests.Activity.cs +++ /dev/null @@ -1,93 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Health.Dicom.Core.Features.Export; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Core.Models.Common; -using Microsoft.Health.Dicom.Core.Models.Export; -using Microsoft.Health.Dicom.Functions.Export; -using Microsoft.Health.Dicom.Functions.Export.Models; -using Microsoft.Health.Operations; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Functions.UnitTests.Export; - -public partial class ExportDurableFunctionTests -{ - [Fact] - public async Task GivenBatch_WhenExporting_ThenShouldCopyFiles() - { - var operationId = Guid.NewGuid(); - var expectedData = new ReadResult[] - { - ReadResult.ForInstance(new InstanceMetadata(new VersionedInstanceIdentifier("1", "2", "3", 100), new InstanceProperties())), - ReadResult.ForInstance(new InstanceMetadata(new VersionedInstanceIdentifier("4", "5", "6", 101), new InstanceProperties())), - ReadResult.ForFailure(new ReadFailureEventArgs(DicomIdentifier.ForSeries("7", "8"), new IOException())), - ReadResult.ForInstance(new InstanceMetadata(new VersionedInstanceIdentifier("9", "1.0", "1.1", 102), new InstanceProperties())), - ReadResult.ForInstance(new InstanceMetadata(new VersionedInstanceIdentifier("121.3", "14", "1.516", 103), new InstanceProperties())) - }; - var expectedInput = new ExportBatchArguments - { - Destination = new ExportDataOptions(DestinationType, new AzureBlobExportOptions()), - Partition = Partition.Default, - Source = new ExportDataOptions(SourceType, new IdentifierExportOptions()), - }; - - // Arrange input, source, and sink - _options.MaxParallelThreads = 2; - - IDurableActivityContext context = Substitute.For(); - context.InstanceId.Returns(operationId.ToString(OperationId.FormatSpecifier)); - context.GetInput().Returns(expectedInput); - - // Note: Parallel.ForEachAsync uses its own CancellationTokenSource - IExportSource source = Substitute.For(); - source.GetAsyncEnumerator(Arg.Any()).Returns(expectedData.ToAsyncEnumerable().GetAsyncEnumerator()); - _sourceProvider.CreateAsync(expectedInput.Source.Settings, expectedInput.Partition).Returns(source); - - IExportSink sink = Substitute.For(); - sink.CopyAsync(expectedData[0], Arg.Any()).Returns(true); - sink.CopyAsync(expectedData[1], Arg.Any()).Returns(false); - sink.CopyAsync(expectedData[2], Arg.Any()).Returns(false); - sink.CopyAsync(expectedData[3], Arg.Any()).Returns(true); - sink.CopyAsync(expectedData[4], Arg.Any()).Returns(true); - _sinkProvider.CreateAsync(expectedInput.Destination.Settings, operationId).Returns(sink); - - // Call the activity - ExportProgress actual = await _function.ExportBatchAsync(context, NullLogger.Instance); - - // Assert behavior - Assert.Equal(new ExportProgress(3, 2), actual); - - context.Received(1).GetInput(); - await _sourceProvider.Received(1).CreateAsync(expectedInput.Source.Settings, expectedInput.Partition); - await _sinkProvider.Received(1).CreateAsync(expectedInput.Destination.Settings, operationId); - source.Received(1).GetAsyncEnumerator(Arg.Any()); - await sink.Received(1).CopyAsync(expectedData[0], Arg.Any()); - await sink.Received(1).CopyAsync(expectedData[1], Arg.Any()); - await sink.Received(1).CopyAsync(expectedData[2], Arg.Any()); - await sink.Received(1).CopyAsync(expectedData[3], Arg.Any()); - await sink.Received(1).CopyAsync(expectedData[4], Arg.Any()); - await sink.Received(1).FlushAsync(default); - } - - [Fact] - public async Task GivenDestination_WhenComplete_ThenInvokeCorrectMethod() - { - var expected = new AzureBlobExportOptions(); - await _function.CompleteCopyAsync(new ExportDataOptions(DestinationType, expected)); - - await _sinkProvider.Received(1).CompleteCopyAsync(expected, default); - } -} diff --git a/src/Microsoft.Health.Dicom.Functions.UnitTests/Export/ExportDurableFunctionTests.Orchestration.cs b/src/Microsoft.Health.Dicom.Functions.UnitTests/Export/ExportDurableFunctionTests.Orchestration.cs deleted file mode 100644 index 73a0e30d37..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.UnitTests/Export/ExportDurableFunctionTests.Orchestration.cs +++ /dev/null @@ -1,282 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Threading.Tasks; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Health.Dicom.Core.Features.Export; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Core.Models.Export; -using Microsoft.Health.Dicom.Functions.Export; -using Microsoft.Health.Dicom.Functions.Export.Models; -using Microsoft.Health.Operations; -using Microsoft.Health.Operations.Functions.Management; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Functions.UnitTests.Export; - -public partial class ExportDurableFunctionTests -{ - [Fact] - public async Task GivenNewOrchestration_WhenExportingFiles_ThenExportBatches() - { - string operationId = OperationId.Generate(); - DateTime createdTime = DateTime.UtcNow; - var input = new ExportCheckpoint - { - Batching = new BatchingOptions - { - Size = 3, - MaxParallelCount = 2, - }, - Destination = new ExportDataOptions(DestinationType, new AzureBlobExportOptions()), - ErrorHref = new Uri($"http://storage/errors/{operationId}.json"), - Partition = Partition.Default, - Source = new ExportDataOptions(SourceType, new IdentifierExportOptions()), - }; - var batches = new ExportDataOptions[] - { - new ExportDataOptions(SourceType, new IdentifierExportOptions()), - new ExportDataOptions(SourceType, new IdentifierExportOptions()), - }; - var results = new ExportProgress[] - { - new ExportProgress(2, 1), - new ExportProgress(3, 0), - }; - var nextSource = new ExportDataOptions(SourceType, new IdentifierExportOptions()); - - // Arrange the input - IDurableOrchestrationContext context = CreateContext(operationId); - - IExportSource source = Substitute.For(); - source - .TryDequeueBatch(3, out Arg.Any>()) - .Returns( - x => { x[1] = batches[0]; return true; }, - x => { x[1] = batches[1]; return true; }); - source.Description.Returns(nextSource); - _sourceProvider.CreateAsync(input.Source.Settings, input.Partition).Returns(source); - - context - .GetInput() - .Returns(input); - context - .CallActivityWithRetryAsync( - nameof(ExportDurableFunction.ExportBatchAsync), - _options.RetryOptions, - Arg.Is(x => ReferenceEquals(x.Source, batches[0]) && ReferenceEquals(x.Destination, input.Destination))) - .Returns(results[0]); - context - .CallActivityWithRetryAsync( - nameof(ExportDurableFunction.ExportBatchAsync), - _options.RetryOptions, - Arg.Is(x => ReferenceEquals(x.Source, batches[1]) && ReferenceEquals(x.Destination, input.Destination))) - .Returns(results[1]); - context - .CallActivityWithRetryAsync( - nameof(DurableOrchestrationClientActivity.GetInstanceStatusAsync), - _options.RetryOptions, - Arg.Any()) - .Returns(new DurableOrchestrationStatus { CreatedTime = createdTime }); - - // Invoke the orchestration - await _function.ExportDicomFilesAsync(context, NullLogger.Instance); - - // Assert behavior - context - .Received(1) - .GetInput(); - await _sourceProvider.Received(1).CreateAsync(input.Source.Settings, input.Partition); - source - .Received(2) - .TryDequeueBatch(3, out Arg.Any>()); - await context - .Received(1) - .CallActivityWithRetryAsync( - nameof(ExportDurableFunction.ExportBatchAsync), - _options.RetryOptions, - Arg.Is(x => ReferenceEquals(x.Source, batches[0]) && ReferenceEquals(x.Destination, input.Destination))); - await context - .Received(1) - .CallActivityWithRetryAsync( - nameof(ExportDurableFunction.ExportBatchAsync), - _options.RetryOptions, - Arg.Is(x => ReferenceEquals(x.Source, batches[1]) && ReferenceEquals(x.Destination, input.Destination))); - await context - .Received(1) - .CallActivityWithRetryAsync( - nameof(DurableOrchestrationClientActivity.GetInstanceStatusAsync), - _options.RetryOptions, - Arg.Any()); - await context - .DidNotReceive() - .CallActivityWithRetryAsync( - nameof(ExportDurableFunction.CompleteCopyAsync), - _options.RetryOptions, - Arg.Any>()); - context - .Received(1) - .ContinueAsNew( - Arg.Is(x => ReferenceEquals(x.Source, nextSource) - && x.ErrorHref == input.ErrorHref - && x.CreatedTime == createdTime - && x.Progress == new ExportProgress(5, 1)), - false); - } - - [Fact] - public async Task GivenExistingOrchestration_WhenExportingFiles_ThenExportBatches() - { - string operationId = OperationId.Generate(); - var checkpoint = new ExportCheckpoint - { - Batching = new BatchingOptions - { - Size = 3, - MaxParallelCount = 2, - }, - CreatedTime = DateTime.UtcNow, - Destination = new ExportDataOptions(DestinationType, new AzureBlobExportOptions()), - ErrorHref = new Uri($"http://storage/errors/{operationId}.json"), - Partition = Partition.Default, - Progress = new ExportProgress(1234, 56), - Source = new ExportDataOptions(SourceType, new IdentifierExportOptions()), - }; - var batch = new ExportDataOptions(SourceType, new IdentifierExportOptions()); - var newProgress = new ExportProgress(2, 0); - - // Arrange the input - IDurableOrchestrationContext context = CreateContext(operationId); - - IExportSource source = Substitute.For(); - source - .TryDequeueBatch(3, out Arg.Any>()) - .Returns( - x => { x[1] = batch; return true; }, - x => { x[1] = null; return false; }); - source.Description.Returns((ExportDataOptions)null); - _sourceProvider.CreateAsync(checkpoint.Source.Settings, checkpoint.Partition).Returns(source); - - context - .GetInput() - .Returns(checkpoint); - context - .CallActivityWithRetryAsync( - nameof(ExportDurableFunction.ExportBatchAsync), - _options.RetryOptions, - Arg.Is(x => ReferenceEquals(x.Source, batch) && ReferenceEquals(x.Destination, checkpoint.Destination))) - .Returns(newProgress); - - // Invoke the orchestration - await _function.ExportDicomFilesAsync(context, NullLogger.Instance); - - // Assert behavior - context - .Received(1) - .GetInput(); - await _sourceProvider.Received(1).CreateAsync(checkpoint.Source.Settings, checkpoint.Partition); - source - .Received(2) - .TryDequeueBatch(3, out Arg.Any>()); - await context - .Received(1) - .CallActivityWithRetryAsync( - nameof(ExportDurableFunction.ExportBatchAsync), - _options.RetryOptions, - Arg.Is(x => ReferenceEquals(x.Source, batch) && ReferenceEquals(x.Destination, checkpoint.Destination))); - await context - .DidNotReceive() - .CallActivityWithRetryAsync( - nameof(DurableOrchestrationClientActivity.GetInstanceStatusAsync), - Arg.Any(), - Arg.Any()); - await context - .DidNotReceive() - .CallActivityWithRetryAsync( - nameof(ExportDurableFunction.CompleteCopyAsync), - _options.RetryOptions, - Arg.Any>()); - context - .Received(1) - .ContinueAsNew( - Arg.Is(x => - x.Batching == checkpoint.Batching && - x.CreatedTime == checkpoint.CreatedTime && - x.Destination == checkpoint.Destination && - x.ErrorHref == checkpoint.ErrorHref && - x.Progress == new ExportProgress(1236, 56) && - x.Source == null && - x.Partition == checkpoint.Partition - ), - false); - } - - [Fact] - public async Task GivenCompletedOrchestration_WhenExportingFiles_ThenFinish() - { - string operationId = OperationId.Generate(); - var checkpoint = new ExportCheckpoint - { - Batching = new BatchingOptions - { - Size = 3, - MaxParallelCount = 2, - }, - CreatedTime = DateTime.UtcNow, - Destination = new ExportDataOptions(DestinationType, new AzureBlobExportOptions()), - ErrorHref = new Uri($"http://storage/errors/{operationId}.json"), - Partition = Partition.Default, - Progress = new ExportProgress(78910, 0), - Source = null, - }; - - // Arrange the input - IDurableOrchestrationContext context = CreateContext(operationId); - - context - .GetInput() - .Returns(checkpoint); - - // Invoke the orchestration - await _function.ExportDicomFilesAsync(context, NullLogger.Instance); - - // Assert behavior - context - .Received(1) - .GetInput(); - await _sourceProvider.DidNotReceiveWithAnyArgs().CreateAsync(default, default, default); - await context - .DidNotReceive() - .CallActivityWithRetryAsync( - nameof(ExportDurableFunction.ExportBatchAsync), - Arg.Any(), - Arg.Any()); - await context - .DidNotReceive() - .CallActivityWithRetryAsync( - nameof(DurableOrchestrationClientActivity.GetInstanceStatusAsync), - Arg.Any(), - Arg.Any()); - await context - .Received(1) - .CallActivityWithRetryAsync( - nameof(ExportDurableFunction.CompleteCopyAsync), - _options.RetryOptions, - checkpoint.Destination); - context - .DidNotReceiveWithAnyArgs() - .ContinueAsNew(default, default); - } - - private static IDurableOrchestrationContext CreateContext(string operationId) - { - IDurableOrchestrationContext context = Substitute.For(); - context.InstanceId.Returns(operationId); - return context; - } -} diff --git a/src/Microsoft.Health.Dicom.Functions.UnitTests/Export/ExportDurableFunctionTests.cs b/src/Microsoft.Health.Dicom.Functions.UnitTests/Export/ExportDurableFunctionTests.cs deleted file mode 100644 index 21b4226e1f..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.UnitTests/Export/ExportDurableFunctionTests.cs +++ /dev/null @@ -1,42 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Features.Export; -using Microsoft.Health.Dicom.Core.Models.Export; -using Microsoft.Health.Dicom.Functions.Export; -using Microsoft.Health.Operations.Functions.DurableTask; -using NSubstitute; - -namespace Microsoft.Health.Dicom.Functions.UnitTests.Export; - -public partial class ExportDurableFunctionTests -{ - private const ExportSourceType SourceType = ExportSourceType.Identifiers; - private const ExportDestinationType DestinationType = ExportDestinationType.AzureBlob; - - private readonly ExportDurableFunction _function; - private readonly IExportSourceProvider _sourceProvider; - private readonly IExportSinkProvider _sinkProvider; - private readonly ExportOptions _options; - - public ExportDurableFunctionTests() - { - _sourceProvider = Substitute.For(); - _sinkProvider = Substitute.For(); - _options = new ExportOptions - { - MaxParallelThreads = 1, - RetryOptions = new ActivityRetryOptions { MaxNumberOfAttempts = 5 } - }; - - _sourceProvider.Type.Returns(SourceType); - _sinkProvider.Type.Returns(DestinationType); - _function = new ExportDurableFunction( - new ExportSourceFactory(new IExportSourceProvider[] { _sourceProvider }), - new ExportSinkFactory(new IExportSinkProvider[] { _sinkProvider }), - Options.Create(_options)); - } -} diff --git a/src/Microsoft.Health.Dicom.Functions.UnitTests/IndexMetricsCollection/IndexMetricsCollectionFunctionTests.cs b/src/Microsoft.Health.Dicom.Functions.UnitTests/IndexMetricsCollection/IndexMetricsCollectionFunctionTests.cs deleted file mode 100644 index 17000b6c62..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.UnitTests/IndexMetricsCollection/IndexMetricsCollectionFunctionTests.cs +++ /dev/null @@ -1,56 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading.Tasks; -using Microsoft.Azure.WebJobs; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Store; -using Microsoft.Health.Dicom.Functions.MetricsCollection; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Functions.UnitTests.IndexMetricsCollection; - -public class IndexMetricsCollectionFunctionTests -{ - private readonly IndexMetricsCollectionFunction _collectionFunction; - private readonly IIndexDataStore _indexStore; - private readonly TimerInfo _timer; - - public IndexMetricsCollectionFunctionTests() - { - _indexStore = Substitute.For(); - _collectionFunction = new IndexMetricsCollectionFunction( - _indexStore, - Options.Create(new FeatureConfiguration { EnableExternalStore = true, })); - _timer = Substitute.For(default, default, default); - } - - [Fact] - public async Task GivenIndexMetricsCollectionFunction_WhenRun_CollectionExecutedWhenExternalStoreEnabled() - { - _indexStore.GetIndexedFileMetricsAsync().ReturnsForAnyArgs(new IndexedFileProperties()); - - await _collectionFunction.Run(_timer, NullLogger.Instance); - - await _indexStore.ReceivedWithAnyArgs(1).GetIndexedFileMetricsAsync(); - } - - [Fact] - public async Task GivenIndexMetricsCollectionFunction_WhenRun_CollectionNotExecutedWhenExternalStoreNotEnabled() - { - _indexStore.GetIndexedFileMetricsAsync().ReturnsForAnyArgs(new IndexedFileProperties()); - var collectionFunctionWihtoutExternalStore = new IndexMetricsCollectionFunction( - _indexStore, - Options.Create(new FeatureConfiguration { EnableExternalStore = false, })); - - await collectionFunctionWihtoutExternalStore.Run(_timer, NullLogger.Instance); - - await _indexStore.DidNotReceiveWithAnyArgs().GetIndexedFileMetricsAsync(); - } -} diff --git a/src/Microsoft.Health.Dicom.Functions.UnitTests/Indexing/Models/BatchCreationArgumentsTests.cs b/src/Microsoft.Health.Dicom.Functions.UnitTests/Indexing/Models/BatchCreationArgumentsTests.cs deleted file mode 100644 index 1a8143bc60..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.UnitTests/Indexing/Models/BatchCreationArgumentsTests.cs +++ /dev/null @@ -1,29 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using Microsoft.Health.Dicom.Functions.Indexing.Models; -using Xunit; - -namespace Microsoft.Health.Dicom.Functions.UnitTests.Indexing.Models; - -public class BatchCreationArgumentsTests -{ - [Fact] - public void GivenBadValues_WhenContructing_ThenThrowExceptions() - { - Assert.Throws(() => new BatchCreationArguments(1, -2, 3)); - Assert.Throws(() => new BatchCreationArguments(1, 2, -3)); - } - - [Fact] - public void GivenValues_WhenConstructing_ThenAssignProperties() - { - var actual = new BatchCreationArguments(1, 2, 3); - Assert.Equal(1, actual.MaxWatermark); - Assert.Equal(2, actual.BatchSize); - Assert.Equal(3, actual.MaxParallelBatches); - } -} diff --git a/src/Microsoft.Health.Dicom.Functions.UnitTests/Indexing/Models/ReindexBatchArgumentsTests.cs b/src/Microsoft.Health.Dicom.Functions.UnitTests/Indexing/Models/ReindexBatchArgumentsTests.cs deleted file mode 100644 index b0cdbb104e..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.UnitTests/Indexing/Models/ReindexBatchArgumentsTests.cs +++ /dev/null @@ -1,44 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Functions.Indexing.Models; -using Xunit; - -namespace Microsoft.Health.Dicom.Functions.UnitTests.Indexing.Models; - -public class ReindexBatchArgumentsTests -{ - [Fact] - public void GivenBadValues_WhenContructing_ThenThrowExceptions() - { - var queryTags = new List - { - new ExtendedQueryTagStoreEntry(1, "01", "DT", "foo", QueryTagLevel.Instance, ExtendedQueryTagStatus.Adding, QueryStatus.Enabled, 0), - new ExtendedQueryTagStoreEntry(2, "02", "DT", "bar", QueryTagLevel.Study, ExtendedQueryTagStatus.Adding, QueryStatus.Enabled, 0), - }; - var range = new WatermarkRange(5, 10); - - Assert.Throws(() => new ReindexBatchArguments(null, range)); - } - - [Fact] - public void GivenValues_WhenConstructing_ThenAssignProperties() - { - var queryTags = new List - { - new ExtendedQueryTagStoreEntry(1, "01", "DT", "foo", QueryTagLevel.Instance, ExtendedQueryTagStatus.Adding, QueryStatus.Enabled, 0), - new ExtendedQueryTagStoreEntry(2, "02", "DT", "bar", QueryTagLevel.Study, ExtendedQueryTagStatus.Adding, QueryStatus.Enabled, 0), - }; - var range = new WatermarkRange(5, 10); - - var actual = new ReindexBatchArguments(queryTags, range); - Assert.Same(queryTags, actual.QueryTags); - Assert.Equal(range, actual.WatermarkRange); - } -} diff --git a/src/Microsoft.Health.Dicom.Functions.UnitTests/Indexing/ReindexDurableFunctionTests.Activity.cs b/src/Microsoft.Health.Dicom.Functions.UnitTests/Indexing/ReindexDurableFunctionTests.Activity.cs deleted file mode 100644 index 7e95c04219..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.UnitTests/Indexing/ReindexDurableFunctionTests.Activity.cs +++ /dev/null @@ -1,354 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Azure; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Models; -using Microsoft.Health.Dicom.Functions.Indexing.Models; -using Microsoft.Health.Dicom.Tests.Common; -using Microsoft.Health.Operations; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Functions.UnitTests.Indexing; - -public partial class ReindexDurableFunctionTests -{ - [Fact] - public async Task GivenTagKeys_WhenAssigningReindexingOperation_ThenShouldPassArguments() - { - Guid operationId = Guid.NewGuid(); - var expectedInput = new List { 1, 2, 3, 4, 5 }; - var expectedOutput = new List - { - new ExtendedQueryTagStoreEntry(1, "01010101", "AS", null, QueryTagLevel.Instance, ExtendedQueryTagStatus.Adding, QueryStatus.Enabled, 0) - }; - - // Arrange input - IDurableActivityContext context = Substitute.For(); - context.InstanceId.Returns(operationId.ToString(OperationId.FormatSpecifier)); - context.GetInput>().Returns(expectedInput); - - _extendedQueryTagStore - .AssignReindexingOperationAsync(expectedInput, operationId, false, CancellationToken.None) - .Returns(expectedOutput); - - // Call the activity - IReadOnlyList actual = await _reindexDurableFunction.AssignReindexingOperationAsync( - context, - NullLogger.Instance); - - // Assert behavior - Assert.Same(expectedOutput, actual); - context.Received(1).GetInput>(); - await _extendedQueryTagStore - .Received(1) - .AssignReindexingOperationAsync(expectedInput, operationId, false, CancellationToken.None); - } - - [Fact] - public async Task GivenTagKeys_WhenGettingExtentendedQueryTags_ThenShouldPassArguments() - { - Guid operationId = Guid.NewGuid(); - var expectedOutput = new List - { - new ExtendedQueryTagStoreEntry(1, "01010101", "AS", null, QueryTagLevel.Instance, ExtendedQueryTagStatus.Adding, QueryStatus.Enabled, 0) - }; - - // Arrange input - IDurableActivityContext context = Substitute.For(); - context.InstanceId.Returns(operationId.ToString(OperationId.FormatSpecifier)); - - _extendedQueryTagStore - .GetExtendedQueryTagsAsync(operationId, CancellationToken.None) - .Returns(expectedOutput); - - // Call the activity - IReadOnlyList actual = await _reindexDurableFunction.GetQueryTagsAsync( - context, - NullLogger.Instance); - - // Assert behavior - Assert.Same(expectedOutput, actual); - await _extendedQueryTagStore - .Received(1) - .GetExtendedQueryTagsAsync(operationId, CancellationToken.None); - } - - [Fact] - [Obsolete] - public async Task GivenLegacyActivityAndNoWatermark_WhenGettingInstanceBatches_ThenShouldInvokeCorrectMethod() - { - IReadOnlyList expected = new List { new WatermarkRange(12345, 678910) }; - _instanceStore - .GetInstanceBatchesAsync(_options.BatchSize, _options.MaxParallelBatches, IndexStatus.Created, null, CancellationToken.None) - .Returns(expected); - - IReadOnlyList actual = await _reindexDurableFunction.GetInstanceBatchesAsync( - null, - NullLogger.Instance); - - Assert.Same(expected, actual); - await _instanceStore - .Received(1) - .GetInstanceBatchesAsync(_options.BatchSize, _options.MaxParallelBatches, IndexStatus.Created, null, CancellationToken.None); - } - - [Fact] - [Obsolete] - public async Task GivenLegacyActivityAndWatermark_WhenGettingInstanceBatches_ThenShouldInvokeCorrectMethod() - { - IReadOnlyList expected = new List { new WatermarkRange(10, 1000) }; - _instanceStore - .GetInstanceBatchesAsync(_options.BatchSize, _options.MaxParallelBatches, IndexStatus.Created, 12345L, CancellationToken.None) - .Returns(expected); - - IReadOnlyList actual = await _reindexDurableFunction.GetInstanceBatchesAsync( - 12345L, - NullLogger.Instance); - - Assert.Same(expected, actual); - await _instanceStore - .Received(1) - .GetInstanceBatchesAsync(_options.BatchSize, _options.MaxParallelBatches, IndexStatus.Created, 12345L, CancellationToken.None); - } - - [Fact] - public async Task GivenNoWatermark_WhenGettingInstanceBatches_ThenShouldInvokeCorrectMethod() - { - const int batchSize = 100; - const int maxParallelBatches = 3; - - IReadOnlyList expected = new List { new WatermarkRange(12345, 678910) }; - _instanceStore - .GetInstanceBatchesAsync(batchSize, maxParallelBatches, IndexStatus.Created, null, CancellationToken.None) - .Returns(expected); - - IReadOnlyList actual = await _reindexDurableFunction.GetInstanceBatchesV2Async( - new BatchCreationArguments(null, batchSize, maxParallelBatches), - NullLogger.Instance); - - Assert.Same(expected, actual); - await _instanceStore - .Received(1) - .GetInstanceBatchesAsync(batchSize, maxParallelBatches, IndexStatus.Created, null, CancellationToken.None); - } - - [Theory] - [InlineData(0)] - [InlineData(1)] - [InlineData(10)] - public async Task GivenWatermark_WhenGettingInstanceBatches_ThenShouldInvokeCorrectMethod(long max) - { - const int batchSize = 100; - const int maxParallelBatches = 3; - - IReadOnlyList expected = new List { new WatermarkRange(1, 2) }; // watermarks don't matter - _instanceStore - .GetInstanceBatchesAsync(batchSize, maxParallelBatches, IndexStatus.Created, max, CancellationToken.None) - .Returns(expected); - - IReadOnlyList actual = await _reindexDurableFunction.GetInstanceBatchesV2Async( - new BatchCreationArguments(max, batchSize, maxParallelBatches), - NullLogger.Instance); - - Assert.Same(expected, actual); - await _instanceStore - .Received(1) - .GetInstanceBatchesAsync(batchSize, maxParallelBatches, IndexStatus.Created, max, CancellationToken.None); - } - - [Fact] - [Obsolete] - public async Task GivenLegacyActivity_WhenReindexing_ThenShouldReindexEachInstance() - { - var batch = new ReindexBatch - { - QueryTags = new List - { - new ExtendedQueryTagStoreEntry(1, "01", "DT", "foo", QueryTagLevel.Instance, ExtendedQueryTagStatus.Adding, QueryStatus.Enabled, 0), - new ExtendedQueryTagStoreEntry(2, "02", "DT", null, QueryTagLevel.Series, ExtendedQueryTagStatus.Adding, QueryStatus.Enabled, 0), - new ExtendedQueryTagStoreEntry(3, "03", "AS", "bar", QueryTagLevel.Study, ExtendedQueryTagStatus.Adding, QueryStatus.Enabled, 0), - }, - WatermarkRange = new WatermarkRange(3, 10), - }; - - var expected = new List - { - new VersionedInstanceIdentifier(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 3), - new VersionedInstanceIdentifier(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 4), - new VersionedInstanceIdentifier(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 5), - new VersionedInstanceIdentifier(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 6), - new VersionedInstanceIdentifier(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 7), - new VersionedInstanceIdentifier(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 8), - new VersionedInstanceIdentifier(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 9), - }; - - // Arrange input - // Note: Parallel.ForEachAsync uses its own CancellationTokenSource - _instanceStore - .GetInstanceIdentifiersByWatermarkRangeAsync(batch.WatermarkRange, IndexStatus.Created, Arg.Any()) - .Returns(expected); - - foreach (VersionedInstanceIdentifier identifier in expected) - { - _instanceReindexer.ReindexInstanceAsync(batch.QueryTags, identifier).Returns(Task.CompletedTask); - } - - // Call the activity - await _reindexDurableFunction.ReindexBatchAsync(batch, NullLogger.Instance); - - // Assert behavior - await _instanceStore - .Received(1) - .GetInstanceIdentifiersByWatermarkRangeAsync(batch.WatermarkRange, IndexStatus.Created, CancellationToken.None); - - foreach (VersionedInstanceIdentifier identifier in expected) - { - await _instanceReindexer.Received(1).ReindexInstanceAsync(batch.QueryTags, identifier, Arg.Any()); - } - } - - [Fact] - public async Task GivenDeletedInstanceInBatch_WhenReindexing_ThenSkip() - => await GivenMissingInstanceInBatch_WhenReindexing_ThenShouldDoubleCheck( - (source, missing) => source.Where((x, i) => i != missing), - t => t); - - [Fact] - public async Task GivenMissingInstanceInBatch_WhenReindexing_ThenThrow() - => await GivenMissingInstanceInBatch_WhenReindexing_ThenShouldDoubleCheck( - (source, skipped) => source, - t => Assert.ThrowsAsync(() => t)); - - private async Task GivenMissingInstanceInBatch_WhenReindexing_ThenShouldDoubleCheck( - Func, int, IEnumerable> getRequeryIdentifiers, - Func assertReindexBatch) - { - var args = new ReindexBatchArguments( - new List - { - new ExtendedQueryTagStoreEntry(1, "01", "DT", "foo", QueryTagLevel.Instance, ExtendedQueryTagStatus.Adding, QueryStatus.Enabled, 0), - }, - new WatermarkRange(6, 8)); - - var identifiers = new List - { - new VersionedInstanceIdentifier(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 6), - new VersionedInstanceIdentifier(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 7), - new VersionedInstanceIdentifier(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 8), - }; - - // Arrange input - // Note: Parallel.ForEachAsync uses its own CancellationTokenSource - _instanceStore - .GetInstanceIdentifiersByWatermarkRangeAsync(args.WatermarkRange, IndexStatus.Created, Arg.Any()) - .Returns( - Task.FromResult>(identifiers), - Task.FromResult>(getRequeryIdentifiers(identifiers, 1).ToList())); - - _instanceReindexer.ReindexInstanceAsync(args.QueryTags, identifiers[0], Arg.Any()).Returns(Task.CompletedTask); - _instanceReindexer.ReindexInstanceAsync(args.QueryTags, identifiers[1], Arg.Any()).Returns(Task.FromException(new ItemNotFoundException(new RequestFailedException("Blob not found")))); - _instanceReindexer.ReindexInstanceAsync(args.QueryTags, identifiers[2], Arg.Any()).Returns(Task.CompletedTask); - - // Call the activity - await assertReindexBatch(_reindexDurableFunction.ReindexBatchV2Async(args, NullLogger.Instance)); - - // Assert behavior - await _instanceStore - .Received(2) - .GetInstanceIdentifiersByWatermarkRangeAsync(args.WatermarkRange, IndexStatus.Created, CancellationToken.None); - - await _instanceReindexer.Received(1).ReindexInstanceAsync(args.QueryTags, identifiers[0], Arg.Any()); - await _instanceReindexer.Received(1).ReindexInstanceAsync(args.QueryTags, identifiers[1], Arg.Any()); - await _instanceReindexer.Received(1).ReindexInstanceAsync(args.QueryTags, identifiers[2], Arg.Any()); - } - - [Fact] - public async Task GivenBatch_WhenReindexing_ThenShouldReindexEachInstance() - { - var args = new ReindexBatchArguments( - new List - { - new ExtendedQueryTagStoreEntry(1, "01", "DT", "foo", QueryTagLevel.Instance, ExtendedQueryTagStatus.Adding, QueryStatus.Enabled, 0), - new ExtendedQueryTagStoreEntry(2, "02", "DT", null, QueryTagLevel.Series, ExtendedQueryTagStatus.Adding, QueryStatus.Enabled, 0), - new ExtendedQueryTagStoreEntry(3, "03", "AS", "bar", QueryTagLevel.Study, ExtendedQueryTagStatus.Adding, QueryStatus.Enabled, 0), - }, - new WatermarkRange(3, 10)); - - var expected = new List - { - new VersionedInstanceIdentifier(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 3), - new VersionedInstanceIdentifier(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 4), - new VersionedInstanceIdentifier(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 5), - new VersionedInstanceIdentifier(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 6), - new VersionedInstanceIdentifier(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 7), - new VersionedInstanceIdentifier(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 8), - new VersionedInstanceIdentifier(TestUidGenerator.Generate(), TestUidGenerator.Generate(), TestUidGenerator.Generate(), 9), - }; - - // Arrange input - // Note: Parallel.ForEachAsync uses its own CancellationTokenSource - _instanceStore - .GetInstanceIdentifiersByWatermarkRangeAsync(args.WatermarkRange, IndexStatus.Created, Arg.Any()) - .Returns(expected); - - foreach (VersionedInstanceIdentifier identifier in expected) - { - _instanceReindexer.ReindexInstanceAsync(args.QueryTags, identifier).Returns(Task.CompletedTask); - } - - // Call the activity - await _reindexDurableFunction.ReindexBatchV2Async(args, NullLogger.Instance); - - // Assert behavior - await _instanceStore - .Received(1) - .GetInstanceIdentifiersByWatermarkRangeAsync(args.WatermarkRange, IndexStatus.Created, CancellationToken.None); - - foreach (VersionedInstanceIdentifier identifier in expected) - { - await _instanceReindexer.Received(1).ReindexInstanceAsync(args.QueryTags, identifier, Arg.Any()); - } - } - - [Fact] - public async Task GivenTagKeys_WhenCompletingReindexing_ThenShouldPassArguments() - { - string operationId = Guid.NewGuid().ToString(); - var expectedInput = new List { 1, 2, 3, 4, 5 }; - var expectedOutput = new List { 1, 2, 4, 5 }; - - // Arrange input - IDurableActivityContext context = Substitute.For(); - context.InstanceId.Returns(operationId); - context.GetInput>().Returns(expectedInput); - - _extendedQueryTagStore - .CompleteReindexingAsync(expectedInput, CancellationToken.None) - .Returns(expectedOutput); - - // Call the activity - IReadOnlyList actual = await _reindexDurableFunction.CompleteReindexingAsync( - context, - NullLogger.Instance); - - // Assert behavior - Assert.Same(expectedOutput, actual); - context.Received(1).GetInput>(); - await _extendedQueryTagStore - .Received(1) - .CompleteReindexingAsync(expectedInput, CancellationToken.None); - } -} diff --git a/src/Microsoft.Health.Dicom.Functions.UnitTests/Indexing/ReindexDurableFunctionTests.Orchestration.cs b/src/Microsoft.Health.Dicom.Functions.UnitTests/Indexing/ReindexDurableFunctionTests.Orchestration.cs deleted file mode 100644 index 0faafb20f7..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.UnitTests/Indexing/ReindexDurableFunctionTests.Orchestration.cs +++ /dev/null @@ -1,525 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Threading.Tasks; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Functions.Indexing; -using Microsoft.Health.Dicom.Functions.Indexing.Models; -using Microsoft.Health.Operations; -using Microsoft.Health.Operations.Functions.Management; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.Functions.UnitTests.Indexing; - -public partial class ReindexDurableFunctionTests -{ - [Fact] - public async Task GivenNewOrchestrationWithWork_WhenReindexingInstances_ThenDivideAndReindexBatches() - { - const int batchSize = 5; - _options.BatchSize = batchSize; - _options.MaxParallelBatches = 3; - - DateTime createdTime = DateTime.UtcNow; - - IReadOnlyList expectedBatches = CreateBatches(50); - var expectedInput = new ReindexCheckpoint { QueryTagKeys = new List { 1, 2, 3, 4, 5 } }; - var expectedTags = new List - { - new ExtendedQueryTagStoreEntry(1, "01010101", "AS", null, QueryTagLevel.Instance, ExtendedQueryTagStatus.Adding, QueryStatus.Enabled, 0), - new ExtendedQueryTagStoreEntry(2, "02020202", "IS", "foo", QueryTagLevel.Series, ExtendedQueryTagStatus.Adding, QueryStatus.Enabled, 0), - new ExtendedQueryTagStoreEntry(4, "04040404", "SH", null, QueryTagLevel.Study, ExtendedQueryTagStatus.Adding, QueryStatus.Enabled, 0) - }; - - // Arrange the input - string operationId = OperationId.Generate(); - IDurableOrchestrationContext context = CreateContext(operationId); - context - .GetInput() - .Returns(expectedInput); - context - .CallActivityWithRetryAsync>( - nameof(ReindexDurableFunction.AssignReindexingOperationAsync), - _options.RetryOptions, - expectedInput.QueryTagKeys) - .Returns(expectedTags); - context - .CallActivityWithRetryAsync>( - nameof(ReindexDurableFunction.GetInstanceBatchesV2Async), - _options.RetryOptions, - Arg.Is(GetPredicate(null))) - .Returns(expectedBatches); - context - .CallActivityWithRetryAsync( - nameof(ReindexDurableFunction.ReindexBatchV2Async), - _options.RetryOptions, - Arg.Any()) - .Returns(Task.CompletedTask); - context - .CallActivityWithRetryAsync( - nameof(DurableOrchestrationClientActivity.GetInstanceStatusAsync), - _options.RetryOptions, - Arg.Is(GetPredicate())) - .Returns(new DurableOrchestrationStatus { CreatedTime = createdTime }); - - // Invoke the orchestration - await _reindexDurableFunction.ReindexInstancesAsync(context, NullLogger.Instance); - - // Assert behavior - context - .Received(1) - .GetInput(); - await context - .Received(1) - .CallActivityWithRetryAsync>( - nameof(ReindexDurableFunction.AssignReindexingOperationAsync), - _options.RetryOptions, - expectedInput.QueryTagKeys); - await context - .DidNotReceive() - .CallActivityWithRetryAsync>( - nameof(ReindexDurableFunction.GetQueryTagsAsync), - _options.RetryOptions, - Arg.Any()); - await context - .Received(1) - .CallActivityWithRetryAsync>( - nameof(ReindexDurableFunction.GetInstanceBatchesV2Async), - _options.RetryOptions, - Arg.Is(GetPredicate(null))); - - foreach (WatermarkRange batch in expectedBatches) - { - await context - .Received(1) - .CallActivityWithRetryAsync( - nameof(ReindexDurableFunction.ReindexBatchV2Async), - _options.RetryOptions, - Arg.Is(GetPredicate(expectedTags, batch))); - } - - await context - .DidNotReceive() - .CallActivityWithRetryAsync>( - nameof(ReindexDurableFunction.CompleteReindexingAsync), - _options.RetryOptions, - Arg.Any()); - await context - .Received(1) - .CallActivityWithRetryAsync( - nameof(DurableOrchestrationClientActivity.GetInstanceStatusAsync), - _options.RetryOptions, - Arg.Is(GetPredicate())); - context - .Received(1) - .ContinueAsNew( - Arg.Is(x => GetPredicate(createdTime, expectedTags, expectedBatches, 50)(x)), - false); - } - - [Fact] - public async Task GivenExistingOrchestrationWithWork_WhenReindexingInstances_ThenDivideAndReindexBatches() - { - const int batchSize = 3; - _options.BatchSize = batchSize; - _options.MaxParallelBatches = 2; - - IReadOnlyList expectedBatches = CreateBatches(35); - var expectedInput = new ReindexCheckpoint - { - Completed = new WatermarkRange(36, 42), - CreatedTime = DateTime.UtcNow, - QueryTagKeys = new List { 1, 2, 3, 4, 5 }, - }; - var expectedTags = new List - { - new ExtendedQueryTagStoreEntry(1, "01010101", "AS", null, QueryTagLevel.Instance, ExtendedQueryTagStatus.Adding, QueryStatus.Enabled, 0), - new ExtendedQueryTagStoreEntry(2, "02020202", "IS", "foo", QueryTagLevel.Series, ExtendedQueryTagStatus.Adding, QueryStatus.Enabled, 0), - new ExtendedQueryTagStoreEntry(4, "04040404", "SH", null, QueryTagLevel.Study, ExtendedQueryTagStatus.Adding, QueryStatus.Enabled, 0) - }; - - // Arrange the input - IDurableOrchestrationContext context = CreateContext(); - context - .GetInput() - .Returns(expectedInput); - context - .CallActivityWithRetryAsync>( - nameof(ReindexDurableFunction.GetQueryTagsAsync), - _options.RetryOptions, - input: null) - .Returns(expectedTags); - context - .CallActivityWithRetryAsync>( - nameof(ReindexDurableFunction.GetInstanceBatchesV2Async), - _options.RetryOptions, - Arg.Is(GetPredicate(35L))) - .Returns(expectedBatches); - context - .CallActivityWithRetryAsync( - nameof(ReindexDurableFunction.ReindexBatchV2Async), - _options.RetryOptions, - Arg.Any()) - .Returns(Task.CompletedTask); - - // Invoke the orchestration - await _reindexDurableFunction.ReindexInstancesAsync(context, NullLogger.Instance); - - // Assert behavior - context - .Received(1) - .GetInput(); - await context - .DidNotReceive() - .CallActivityWithRetryAsync>( - nameof(ReindexDurableFunction.AssignReindexingOperationAsync), - _options.RetryOptions, - Arg.Any()); - await context - .Received(1) - .CallActivityWithRetryAsync>( - nameof(ReindexDurableFunction.GetQueryTagsAsync), - _options.RetryOptions, - input: null); - await context - .Received(1) - .CallActivityWithRetryAsync>( - nameof(ReindexDurableFunction.GetInstanceBatchesV2Async), - _options.RetryOptions, - Arg.Is(GetPredicate(35L))); - - foreach (WatermarkRange batch in expectedBatches) - { - await context - .Received(1) - .CallActivityWithRetryAsync( - nameof(ReindexDurableFunction.ReindexBatchV2Async), - _options.RetryOptions, - Arg.Is(GetPredicate(expectedTags, batch))); - } - - await context - .DidNotReceive() - .CallActivityWithRetryAsync>( - nameof(ReindexDurableFunction.CompleteReindexingAsync), - _options.RetryOptions, - Arg.Any()); - await context - .DidNotReceive() - .CallActivityWithRetryAsync( - nameof(DurableOrchestrationClientActivity.GetInstanceStatusAsync), - _options.RetryOptions, - Arg.Any()); - context - .Received(1) - .ContinueAsNew( - Arg.Is(x => GetPredicate(expectedInput.CreatedTime.Value, expectedTags, expectedBatches, 42)(x)), - false); - } - - [Fact] - public async Task GivenNoInstances_WhenReindexingInstances_ThenComplete() - { - var expectedBatches = new List(); - var expectedInput = new ReindexCheckpoint { QueryTagKeys = new List { 1, 2, 3, 4, 5 } }; - var expectedTags = new List - { - new ExtendedQueryTagStoreEntry(1, "01010101", "AS", null, QueryTagLevel.Instance, ExtendedQueryTagStatus.Adding, QueryStatus.Enabled, 0), - new ExtendedQueryTagStoreEntry(2, "02020202", "IS", "foo", QueryTagLevel.Series, ExtendedQueryTagStatus.Adding, QueryStatus.Enabled, 0), - new ExtendedQueryTagStoreEntry(4, "04040404", "SH", null, QueryTagLevel.Study, ExtendedQueryTagStatus.Adding, QueryStatus.Enabled, 0) - }; - - // Arrange the input - IDurableOrchestrationContext context = CreateContext(); - context - .GetInput() - .Returns(expectedInput); - context - .CallActivityWithRetryAsync>( - nameof(ReindexDurableFunction.AssignReindexingOperationAsync), - _options.RetryOptions, - expectedInput.QueryTagKeys) - .Returns(expectedTags); - context - .CallActivityWithRetryAsync>( - nameof(ReindexDurableFunction.GetInstanceBatchesV2Async), - _options.RetryOptions, - Arg.Is(GetPredicate(null))) - .Returns(expectedBatches); - context - .CallActivityWithRetryAsync>( - nameof(ReindexDurableFunction.CompleteReindexingAsync), - _options.RetryOptions, - Arg.Is>(x => x.SequenceEqual(expectedTags.Select(x => x.Key)))) - .Returns(expectedTags.Select(x => x.Key).ToList()); - - // Invoke the orchestration - await _reindexDurableFunction.ReindexInstancesAsync(context, NullLogger.Instance); - - // Assert behavior - context - .Received(1) - .GetInput(); - await context - .Received(1) - .CallActivityWithRetryAsync>( - nameof(ReindexDurableFunction.AssignReindexingOperationAsync), - _options.RetryOptions, - expectedInput.QueryTagKeys); - await context - .DidNotReceive() - .CallActivityWithRetryAsync>( - nameof(ReindexDurableFunction.GetQueryTagsAsync), - _options.RetryOptions, - Arg.Any()); - await context - .Received(1) - .CallActivityWithRetryAsync>( - nameof(ReindexDurableFunction.GetInstanceBatchesV2Async), - _options.RetryOptions, - Arg.Is(GetPredicate(null))); - await context - .DidNotReceive() - .CallActivityWithRetryAsync( - nameof(ReindexDurableFunction.ReindexBatchV2Async), - _options.RetryOptions, - Arg.Any()); - await context - .Received(1) - .CallActivityWithRetryAsync>( - nameof(ReindexDurableFunction.CompleteReindexingAsync), - _options.RetryOptions, - Arg.Is>(x => x.SequenceEqual(expectedTags.Select(x => x.Key)))); - await context - .DidNotReceive() - .CallActivityWithRetryAsync( - nameof(DurableOrchestrationClientActivity.GetInstanceStatusAsync), - _options.RetryOptions, - Arg.Any()); - context - .DidNotReceiveWithAnyArgs() - .ContinueAsNew(default, default); - } - - [Theory] - [InlineData(1, 100)] - [InlineData(5, 1000)] - public async Task GivenNoRemainingInstances_WhenReindexingInstances_ThenComplete(long start, long end) - { - var expectedBatches = new List(); - var expectedInput = new ReindexCheckpoint - { - Completed = new WatermarkRange(start, end), - CreatedTime = DateTime.UtcNow, - QueryTagKeys = new List { 1, 2, 3, 4, 5 }, - }; - var expectedTags = new List - { - new ExtendedQueryTagStoreEntry(1, "01010101", "AS", null, QueryTagLevel.Instance, ExtendedQueryTagStatus.Adding, QueryStatus.Enabled, 0), - new ExtendedQueryTagStoreEntry(2, "02020202", "IS", "foo", QueryTagLevel.Series, ExtendedQueryTagStatus.Adding, QueryStatus.Enabled, 0), - new ExtendedQueryTagStoreEntry(4, "04040404", "SH", null, QueryTagLevel.Study, ExtendedQueryTagStatus.Adding, QueryStatus.Enabled, 0) - }; - - // Arrange the input - IDurableOrchestrationContext context = CreateContext(); - context - .GetInput() - .Returns(expectedInput); - context - .CallActivityWithRetryAsync>( - nameof(ReindexDurableFunction.GetQueryTagsAsync), - _options.RetryOptions, - input: null) - .Returns(expectedTags); - context - .CallActivityWithRetryAsync>( - nameof(ReindexDurableFunction.GetInstanceBatchesV2Async), - _options.RetryOptions, - Arg.Is(GetPredicate(start - 1))) - .Returns(expectedBatches); - context - .CallActivityWithRetryAsync>( - nameof(ReindexDurableFunction.CompleteReindexingAsync), - _options.RetryOptions, - Arg.Is>(x => x.SequenceEqual(expectedTags.Select(x => x.Key)))) - .Returns(expectedTags.Select(x => x.Key).ToList()); - - // Invoke the orchestration - await _reindexDurableFunction.ReindexInstancesAsync(context, NullLogger.Instance); - - // Assert behavior - context - .Received(1) - .GetInput(); - await context - .DidNotReceive() - .CallActivityWithRetryAsync>( - nameof(ReindexDurableFunction.AssignReindexingOperationAsync), - _options.RetryOptions, - Arg.Any()); - await context - .Received(1) - .CallActivityWithRetryAsync>( - nameof(ReindexDurableFunction.GetQueryTagsAsync), - _options.RetryOptions, - input: null); - await context - .Received(1) - .CallActivityWithRetryAsync>( - nameof(ReindexDurableFunction.GetInstanceBatchesV2Async), - _options.RetryOptions, - Arg.Is(GetPredicate(start - 1))); - await context - .DidNotReceive() - .CallActivityWithRetryAsync( - nameof(ReindexDurableFunction.ReindexBatchV2Async), - _options.RetryOptions, - Arg.Any()); - await context - .Received(1) - .CallActivityWithRetryAsync>( - nameof(ReindexDurableFunction.CompleteReindexingAsync), - _options.RetryOptions, - Arg.Is>(x => x.SequenceEqual(expectedTags.Select(x => x.Key)))); - await context - .DidNotReceive() - .CallActivityWithRetryAsync( - nameof(DurableOrchestrationClientActivity.GetInstanceStatusAsync), - _options.RetryOptions, - Arg.Any()); - context - .DidNotReceiveWithAnyArgs() - .ContinueAsNew(default, default); - } - - [Fact] - public async Task GivenNoQueryTags_WhenReindexingInstances_ThenComplete() - { - var expectedInput = new ReindexCheckpoint { QueryTagKeys = new List { 1, 2, 3, 4, 5 } }; - var expectedTags = new List(); - - // Arrange the input - IDurableOrchestrationContext context = CreateContext(); - context - .GetInput() - .Returns(expectedInput); - context - .CallActivityWithRetryAsync>( - nameof(ReindexDurableFunction.AssignReindexingOperationAsync), - _options.RetryOptions, - expectedInput.QueryTagKeys) - .Returns(expectedTags); - - // Invoke the orchestration - await _reindexDurableFunction.ReindexInstancesAsync(context, NullLogger.Instance); - - // Assert behavior - context - .Received(1) - .GetInput(); - await context - .Received(1) - .CallActivityWithRetryAsync>( - nameof(ReindexDurableFunction.AssignReindexingOperationAsync), - _options.RetryOptions, - expectedInput.QueryTagKeys); - await context - .DidNotReceive() - .CallActivityWithRetryAsync>( - nameof(ReindexDurableFunction.GetQueryTagsAsync), - _options.RetryOptions, - Arg.Any()); - await context - .DidNotReceive() - .CallActivityWithRetryAsync>( - nameof(ReindexDurableFunction.GetInstanceBatchesV2Async), - _options.RetryOptions, - Arg.Any()); - await context - .DidNotReceive() - .CallActivityWithRetryAsync( - nameof(ReindexDurableFunction.ReindexBatchV2Async), - _options.RetryOptions, - Arg.Any()); - await context - .DidNotReceive() - .CallActivityWithRetryAsync>( - nameof(ReindexDurableFunction.CompleteReindexingAsync), - _options.RetryOptions, - Arg.Any()); - await context - .DidNotReceive() - .CallActivityWithRetryAsync( - nameof(DurableOrchestrationClientActivity.GetInstanceStatusAsync), - _options.RetryOptions, - Arg.Any()); - context - .DidNotReceiveWithAnyArgs() - .ContinueAsNew(default, default); - } - - private static IDurableOrchestrationContext CreateContext() - => CreateContext(OperationId.Generate()); - - private static IDurableOrchestrationContext CreateContext(string operationId) - { - IDurableOrchestrationContext context = Substitute.For(); - context.InstanceId.Returns(operationId); - return context; - } - - private IReadOnlyList CreateBatches(long end) - { - var batches = new List(); - - long current = end; - for (int i = 0; i < _options.MaxParallelBatches && current > 0; i++) - { - batches.Add(new WatermarkRange(Math.Max(1, current - _options.BatchSize + 1), current)); - current -= _options.BatchSize; - } - - return batches; - } - - private Expression> GetPredicate(long? maxWatermark) - { - return x => x.MaxWatermark == maxWatermark - && x.BatchSize == _options.BatchSize - && x.MaxParallelBatches == _options.MaxParallelBatches; - } - - private static Expression> GetPredicate( - IReadOnlyList queryTags, - WatermarkRange expected) - { - return x => ReferenceEquals(x.QueryTags, queryTags) && x.WatermarkRange == expected; - } - - private static Expression> GetPredicate() - { - return x => !x.ShowHistory && !x.ShowHistoryOutput && !x.ShowInput; - } - - private static Predicate GetPredicate( - DateTime createdTime, - IReadOnlyList queryTags, - IReadOnlyList expectedBatches, - long end) - { - return x => x is ReindexCheckpoint r - && r.QueryTagKeys.SequenceEqual(queryTags.Select(y => y.Key)) - && r.Completed == new WatermarkRange(expectedBatches[expectedBatches.Count - 1].Start, end) - && r.CreatedTime == createdTime; - } -} diff --git a/src/Microsoft.Health.Dicom.Functions.UnitTests/Indexing/ReindexDurableFunctionTests.cs b/src/Microsoft.Health.Dicom.Functions.UnitTests/Indexing/ReindexDurableFunctionTests.cs deleted file mode 100644 index 8268ff650b..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.UnitTests/Indexing/ReindexDurableFunctionTests.cs +++ /dev/null @@ -1,36 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Features.Indexing; -using Microsoft.Health.Dicom.Core.Features.Retrieve; -using Microsoft.Health.Dicom.Functions.Indexing; -using Microsoft.Health.Operations.Functions.DurableTask; -using NSubstitute; - -namespace Microsoft.Health.Dicom.Functions.UnitTests.Indexing; - -public partial class ReindexDurableFunctionTests -{ - private readonly ReindexDurableFunction _reindexDurableFunction; - private readonly IExtendedQueryTagStore _extendedQueryTagStore; - private readonly IInstanceStore _instanceStore; - private readonly IInstanceReindexer _instanceReindexer; - private readonly QueryTagIndexingOptions _options; - - public ReindexDurableFunctionTests() - { - _extendedQueryTagStore = Substitute.For(); - _instanceStore = Substitute.For(); - _instanceReindexer = Substitute.For(); - _options = new QueryTagIndexingOptions { RetryOptions = new ActivityRetryOptions() }; - _reindexDurableFunction = new ReindexDurableFunction( - _extendedQueryTagStore, - _instanceStore, - _instanceReindexer, - Options.Create(_options)); - } -} diff --git a/src/Microsoft.Health.Dicom.Functions.UnitTests/Microsoft.Health.Dicom.Functions.UnitTests.csproj b/src/Microsoft.Health.Dicom.Functions.UnitTests/Microsoft.Health.Dicom.Functions.UnitTests.csproj deleted file mode 100644 index f776eabc57..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.UnitTests/Microsoft.Health.Dicom.Functions.UnitTests.csproj +++ /dev/null @@ -1,50 +0,0 @@ - - - - net6.0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ExportDurableFunctionTests.cs - - - ReindexDurableFunctionTests.cs - - - UpdateDurableFunctionTests.cs - - - DataCleanupDurableFunctionTests.cs - - - ContentLengthBackFillDurableFunction.cs - - - - diff --git a/src/Microsoft.Health.Dicom.Functions.UnitTests/Update/UpdateDurableFunctionTests.Activity.cs b/src/Microsoft.Health.Dicom.Functions.UnitTests/Update/UpdateDurableFunctionTests.Activity.cs deleted file mode 100644 index 1ab329ee22..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.UnitTests/Update/UpdateDurableFunctionTests.Activity.cs +++ /dev/null @@ -1,318 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using FellowOakDicom; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Functions.Update.Models; -using Microsoft.Health.Dicom.Tests.Common; -using NSubstitute; -using NSubstitute.ExceptionExtensions; -using Xunit; - -namespace Microsoft.Health.Dicom.Functions.UnitTests.Update; - -public partial class UpdateDurableFunctionTests -{ - private static readonly FileProperties DefaultFileProperties = new FileProperties - { - Path = "default/path/0.dcm", - ETag = "123" - }; - - [Fact] - public async Task GivenInstanceMetadata_WhenUpdatingInstanceWatermark_ThenShouldMatchCorrectly() - { - var studyInstanceUid = TestUidGenerator.Generate(); - var identifiers = GetInstanceIdentifiersList(studyInstanceUid); - IReadOnlyList expected = identifiers.Select(x => - new InstanceFileState - { - Version = x.VersionedInstanceIdentifier.Version, - OriginalVersion = x.InstanceProperties.OriginalVersion, - NewVersion = x.InstanceProperties.NewVersion - }).ToList(); - - var versions = expected.Select(x => x.Version).ToList(); - - _indexStore.BeginUpdateInstancesAsync(Arg.Any(), studyInstanceUid, CancellationToken.None).Returns(identifiers); - - IEnumerable result = await _updateDurableFunction.UpdateInstanceWatermarkV2Async( - new UpdateInstanceWatermarkArgumentsV2(Partition.Default, studyInstanceUid), - NullLogger.Instance); - IReadOnlyList actual = result.ToList(); - - await _indexStore - .Received(1) - .BeginUpdateInstancesAsync(Arg.Any(), studyInstanceUid, cancellationToken: CancellationToken.None); - - for (int i = 0; i < expected.Count; i++) - { - Assert.Equal(expected[i].Version, actual[i].VersionedInstanceIdentifier.Version); - Assert.Equal(expected[i].OriginalVersion, actual[i].InstanceProperties.OriginalVersion); - Assert.Equal(expected[i].NewVersion, actual[i].InstanceProperties.NewVersion); - } - } - - [Fact] - public async Task GivenInstanceMetadata_WhenUpdatingBlobInBatches_ThenShouldUpdateCorrectly() - { - var studyInstanceUid = TestUidGenerator.Generate(); - var expected = GetInstanceIdentifiersList(studyInstanceUid); - - var dataset = "{\"00100010\":{\"vr\":\"PN\",\"Value\":[{\"Alphabetic\":\"Patient Name\"}]}}"; - - foreach (var instance in expected) - { - _updateInstanceService - .UpdateInstanceBlobAsync( - instance, - Arg.Is(x => x.GetSingleValue(DicomTag.PatientName) == "Patient Name"), - Partition.Default, - Arg.Any()) - .Returns(DefaultFileProperties); - } - - await _updateDurableFunction.UpdateInstanceBlobsV3Async( - new UpdateInstanceBlobArgumentsV2(Partition.Default, expected, dataset), - NullLogger.Instance); - - foreach (var instance in expected) - { - await _updateInstanceService - .Received(1) - .UpdateInstanceBlobAsync(Arg.Is(GetPredicate(instance)), Arg.Is(x => x.GetSingleValue(DicomTag.PatientName) == "Patient Name"), - Partition.Default, - Arg.Any()); - } - } - - [Fact] - public async Task GivenInstanceMetadata_WhenUpdatingWithDataStoreException_ThenShouldReturnFailureCorrectly() - { - var studyInstanceUid = TestUidGenerator.Generate(); - var expected = GetInstanceIdentifiersList(studyInstanceUid); - - var dataset = "{\"00100010\":{\"vr\":\"PN\",\"Value\":[{\"Alphabetic\":\"Patient Name\"}]}}"; - - foreach (var instance in expected) - { - _updateInstanceService - .UpdateInstanceBlobAsync( - instance, - Arg.Is(x => x.GetSingleValue(DicomTag.PatientName) == "Patient Name"), - Partition.Default, - Arg.Any()) - .ThrowsAsync(new DataStoreException("Error")); - } - - var response = await _updateDurableFunction.UpdateInstanceBlobsV3Async( - new UpdateInstanceBlobArgumentsV2(Partition.Default, expected, dataset), - NullLogger.Instance); - - foreach (var instance in expected) - { - await _updateInstanceService - .Received(1) - .UpdateInstanceBlobAsync(Arg.Is(GetPredicate(instance)), Arg.Is(x => x.GetSingleValue(DicomTag.PatientName) == "Patient Name"), - Partition.Default, - Arg.Any()); - } - - Assert.Equal(expected.Count, response.Errors.Count); - } - - [Fact] - public async Task GivenCompleteInstanceArgument_WhenCompleting_ThenShouldComplete() - { - var studyInstanceUid = TestUidGenerator.Generate(); - - var instanceMetadataList = new List(); - _indexStore.EndUpdateInstanceAsync(Partition.DefaultKey, studyInstanceUid, new DicomDataset(), instanceMetadataList, Array.Empty(), CancellationToken.None).Returns(Task.CompletedTask); - - var ds = new DicomDataset - { - { DicomTag.PatientName, "Patient Name" } - }; - - await _updateDurableFunction.CompleteUpdateStudyV4Async( - new CompleteStudyArgumentsV2( - Partition.DefaultKey, - studyInstanceUid, - "{\"00100010\":{\"vr\":\"PN\",\"Value\":[{\"Alphabetic\":\"Patient Name\"}]}}", - instanceMetadataList), - NullLogger.Instance); - - await _indexStore - .Received(1) - .EndUpdateInstanceAsync(Partition.DefaultKey, studyInstanceUid, Arg.Is(x => x.GetSingleValue(DicomTag.PatientName) == "Patient Name"), instanceMetadataList, Arg.Any>(), CancellationToken.None); - } - - [Fact] - public async Task GivenInstanceUpdateFails_WhenDeleteFileWithV3_ThenShouldDeleteNewVersionSuccessfullyWithoutFileProperties() - { - var studyInstanceUid = TestUidGenerator.Generate(); - var identifiers = GetInstanceIdentifiersList(studyInstanceUid, instanceProperty: new InstanceProperties { NewVersion = 1 }).Take(1).ToList(); - - Assert.Null(identifiers[0].InstanceProperties.FileProperties); - - _updateInstanceService - .DeleteInstanceBlobAsync(identifiers[0].InstanceProperties.NewVersion.Value, identifiers[0].VersionedInstanceIdentifier.Partition, null, Arg.Any()) - .Returns(Task.CompletedTask); - - // Call the activity - await _updateDurableFunction.CleanupNewVersionBlobV3Async( - new CleanupBlobArgumentsV2(identifiers, Partition.Default), - NullLogger.Instance); - - // Assert behavior - await _updateInstanceService - .Received(1) - .DeleteInstanceBlobAsync(identifiers[0].InstanceProperties.NewVersion.Value, identifiers[0].VersionedInstanceIdentifier.Partition, null, Arg.Any()); - } - - [Fact] - public async Task GivenInstanceUpdateFails_WhenDeleteFileWithV3_ThenShouldDeleteNewVersionSuccessfullyWithFileProperties() - { - var studyInstanceUid = TestUidGenerator.Generate(); - var identifiers = GetInstanceIdentifiersList(studyInstanceUid, instanceProperty: new InstanceProperties { NewVersion = 1, FileProperties = DefaultFileProperties }).Take(1).ToList(); - - _updateInstanceService - .DeleteInstanceBlobAsync(Arg.Any(), Partition.Default, DefaultFileProperties, Arg.Any()) - .Returns(Task.CompletedTask); - - // Call the activity - await _updateDurableFunction.CleanupNewVersionBlobV3Async( - new CleanupBlobArgumentsV2(identifiers, Partition.Default), - NullLogger.Instance); - - // Assert behavior - await _updateInstanceService - .Received(1) - .DeleteInstanceBlobAsync(Arg.Any(), Partition.Default, DefaultFileProperties, Arg.Any()); - } - - [Fact] - public async Task GivenInstanceMetadataList_WhenDeleteFileV3_ThenShouldDeleteSuccessfullyWithoutFileProperties() - { - var studyInstanceUid = TestUidGenerator.Generate(); - var identifiers = GetInstanceIdentifiersList( - studyInstanceUid, - partition: Partition.Default, - instanceProperty: new InstanceProperties { OriginalVersion = 1 }) - .Take(1).ToList(); - - Assert.Null(identifiers[0].InstanceProperties.FileProperties); - - _updateInstanceService - .DeleteInstanceBlobAsync(identifiers[0].VersionedInstanceIdentifier.Version, identifiers[0].VersionedInstanceIdentifier.Partition, null, Arg.Any()) - .Returns(Task.CompletedTask); - - // Call the activity - await _updateDurableFunction.DeleteOldVersionBlobV3Async( - new CleanupBlobArgumentsV2(identifiers, identifiers[0].VersionedInstanceIdentifier.Partition), - NullLogger.Instance); - - // Assert behavior - await _updateInstanceService - .Received(1) - .DeleteInstanceBlobAsync(identifiers[0].VersionedInstanceIdentifier.Version, identifiers[0].VersionedInstanceIdentifier.Partition, null, Arg.Any()); - } - - [Fact] - public async Task GivenInstanceMetadataList_WhenDeleteFileWithV3_ThenShouldDeleteSuccessfullyWithFileProperties() - { - var studyInstanceUid = TestUidGenerator.Generate(); - var identifiers = GetInstanceIdentifiersList( - studyInstanceUid, - instanceProperty: new InstanceProperties { OriginalVersion = 1, FileProperties = DefaultFileProperties }) - .Take(1).ToList(); - - Assert.NotNull(identifiers[0].InstanceProperties.FileProperties); - - _updateInstanceService - .DeleteInstanceBlobAsync(identifiers[0].VersionedInstanceIdentifier.Version, identifiers[0].VersionedInstanceIdentifier.Partition, identifiers[0].InstanceProperties.FileProperties, Arg.Any()) - .Returns(Task.CompletedTask); - - // Call the activity - await _updateDurableFunction.DeleteOldVersionBlobV3Async( - new CleanupBlobArgumentsV2(identifiers, identifiers[0].VersionedInstanceIdentifier.Partition), - NullLogger.Instance); - - // Assert behavior - await _updateInstanceService - .Received(1) - .DeleteInstanceBlobAsync(identifiers[0].VersionedInstanceIdentifier.Version, identifiers[0].VersionedInstanceIdentifier.Partition, identifiers[0].InstanceProperties.FileProperties, Arg.Any()); - } - - [Fact] - public async Task GivenInstanceMetadataList_WhenChangeAccessTierV2_ThenShouldChangeSuccessfullyUsingFileProperties() - { - var studyInstanceUid = TestUidGenerator.Generate(); - var instances = GetInstanceIdentifiersList(studyInstanceUid, Partition.Default, new InstanceProperties { NewVersion = 2, FileProperties = DefaultFileProperties }); - IReadOnlyList expected = instances.Take(1).ToList(); - - Assert.NotNull(expected[0].InstanceProperties.FileProperties); - - _fileStore - .SetBlobToColdAccessTierAsync(expected[0].VersionedInstanceIdentifier.Version, expected[0].VersionedInstanceIdentifier.Partition, expected[0].InstanceProperties.FileProperties, Arg.Any()) - .Returns(Task.CompletedTask); - - // Call the activity - await _updateDurableFunction.SetOriginalBlobToColdAccessTierV2Async( - new CleanupBlobArgumentsV2(expected, Partition.Default), - NullLogger.Instance); - - // Assert behavior - await _fileStore - .Received(1) - .SetBlobToColdAccessTierAsync(expected[0].VersionedInstanceIdentifier.Version, expected[0].VersionedInstanceIdentifier.Partition, expected[0].InstanceProperties.FileProperties, Arg.Any()); - } - - [Fact] - public async Task GivenInstanceMetadataList_WhenChangeAccessTierV2_ThenShouldChangeSuccessfullyWithoutUsingFileProperties() - { - var studyInstanceUid = TestUidGenerator.Generate(); - var instances = GetInstanceIdentifiersList(studyInstanceUid, Partition.Default, new InstanceProperties { NewVersion = 2 }); - IReadOnlyList expected = instances.Take(1).ToList(); - - Assert.Null(expected[0].InstanceProperties.FileProperties); - - _fileStore - .SetBlobToColdAccessTierAsync(expected[0].VersionedInstanceIdentifier.Version, expected[0].VersionedInstanceIdentifier.Partition, null, Arg.Any()) - .Returns(Task.CompletedTask); - - // Call the activity - await _updateDurableFunction.SetOriginalBlobToColdAccessTierV2Async( - new CleanupBlobArgumentsV2(expected, Partition.Default), - NullLogger.Instance); - - // Assert behavior - await _fileStore - .Received(1) - .SetBlobToColdAccessTierAsync(expected[0].VersionedInstanceIdentifier.Version, expected[0].VersionedInstanceIdentifier.Partition, null, Arg.Any()); - } - - - private static List GetInstanceIdentifiersList(string studyInstanceUid, Partition partition = null, InstanceProperties instanceProperty = null) - { - var dicomInstanceIdentifiersList = new List(); - instanceProperty ??= new InstanceProperties { NewVersion = 2, OriginalVersion = 3 }; - partition ??= Partition.Default; - - dicomInstanceIdentifiersList.Add(new InstanceMetadata(new VersionedInstanceIdentifier(studyInstanceUid, TestUidGenerator.Generate(), TestUidGenerator.Generate(), 0, partition), instanceProperty)); - dicomInstanceIdentifiersList.Add(new InstanceMetadata(new VersionedInstanceIdentifier(studyInstanceUid, TestUidGenerator.Generate(), TestUidGenerator.Generate(), 1, partition), instanceProperty)); - return dicomInstanceIdentifiersList; - } -} diff --git a/src/Microsoft.Health.Dicom.Functions.UnitTests/Update/UpdateDurableFunctionTests.Orchestration.cs b/src/Microsoft.Health.Dicom.Functions.UnitTests/Update/UpdateDurableFunctionTests.Orchestration.cs deleted file mode 100644 index 846bebca8f..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.UnitTests/Update/UpdateDurableFunctionTests.Orchestration.cs +++ /dev/null @@ -1,912 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Threading.Tasks; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Functions.Update; -using Microsoft.Health.Dicom.Functions.Update.Models; -using Microsoft.Health.Dicom.Tests.Common; -using Microsoft.Health.Operations; -using Microsoft.IdentityModel.Tokens; -using NSubstitute; -using NSubstitute.ExceptionExtensions; -using OpenTelemetry.Metrics; -using Xunit; - -namespace Microsoft.Health.Dicom.Functions.UnitTests.Update; - -public partial class UpdateDurableFunctionTests -{ - [Fact] - public async Task GivenV4OrchestrationWithInput_WhenUpdatingInstances_ThenComplete() - { - const int batchSize = 5; - _options.BatchSize = batchSize; - - DateTime createdTime = DateTime.UtcNow; - - var expectedInput = GetUpdateCheckpoint(); - - var expectedInstances = new List - { - new InstanceFileState - { - Version = 1 - }, - new InstanceFileState - { - Version = 2 - } - }; - - var expectedInstancesWithNewWatermark = new List - { - new InstanceFileState - { - Version = 1, - NewVersion = 3, - }, - new InstanceFileState - { - Version = 2, - NewVersion = 4, - } - }; - - List instanceMetadataList = CreateExpectedInstanceMetadataList(expectedInstancesWithNewWatermark); - - // Arrange the input - string operationId = OperationId.Generate(); - IDurableOrchestrationContext context = CreateContext(operationId); - - context - .GetInput() - .Returns(expectedInput); - context - .CallActivityWithRetryAsync>( - nameof(UpdateDurableFunction.UpdateInstanceWatermarkV2Async), - _options.RetryOptions, - Arg.Any()) - .Returns(instanceMetadataList); - context - .CallActivityWithRetryAsync( - nameof(UpdateDurableFunction.UpdateInstanceBlobsV3Async), - _options.RetryOptions, - Arg.Is(GetPredicate(expectedInput.Partition, instanceMetadataList, expectedInput.ChangeDataset)) - ) - .Returns(new UpdateInstanceResponse(instanceMetadataList, new List())); - context - .CallActivityWithRetryAsync( - nameof(UpdateDurableFunction.CompleteUpdateStudyV4Async), - _options.RetryOptions, - Arg.Any()) - .Returns(Task.CompletedTask); - context - .CallActivityWithRetryAsync( - nameof(UpdateDurableFunction.DeleteOldVersionBlobV3Async), - _options.RetryOptions, - Arg.Any()) - .Returns(Task.CompletedTask); - context - .CallActivityWithRetryAsync( - nameof(UpdateDurableFunction.SetOriginalBlobToColdAccessTierV2Async), - _options.RetryOptions, - Arg.Any()) - .Returns(Task.CompletedTask); - - // Invoke the orchestration - await _updateDurableFunction.UpdateInstancesV6Async(context, NullLogger.Instance); - - // Assert behavior - context - .Received(1) - .GetInput(); - await context - .Received(1) - .CallActivityWithRetryAsync>( - nameof(UpdateDurableFunction.UpdateInstanceWatermarkV2Async), - _options.RetryOptions, - Arg.Any()); - await context - .Received(1) - .CallActivityWithRetryAsync( - nameof(UpdateDurableFunction.UpdateInstanceBlobsV3Async), - _options.RetryOptions, - Arg.Is(GetPredicate(expectedInput.Partition, instanceMetadataList, expectedInput.ChangeDataset)) - ); - await context - .Received(1) - .CallActivityWithRetryAsync( - nameof(UpdateDurableFunction.CompleteUpdateStudyV4Async), - _options.RetryOptions, - Arg.Any()); - await context - .Received(1) - .CallActivityWithRetryAsync( - nameof(UpdateDurableFunction.DeleteOldVersionBlobV3Async), - _options.RetryOptions, - Arg.Any()); - await context - .Received(1) - .CallActivityWithRetryAsync( - nameof(UpdateDurableFunction.SetOriginalBlobToColdAccessTierV2Async), - _options.RetryOptions, - Arg.Any()); - context - .Received(1) - .ContinueAsNew( - Arg.Is(x => x.NumberOfStudyCompleted == 1), - false); - } - - [Fact] - public async Task GivenV4OrchestrationWithInputAndExternalStoreEnabled_WhenUpdatingInstances_ThenInstanceMetadataListWithFilePropertiesPassedInToCompleteUpdate() - { - const int batchSize = 5; - _options.BatchSize = batchSize; - - var expectedInput = GetUpdateCheckpoint(); - var studyInstanceUid = expectedInput.StudyInstanceUids[expectedInput.NumberOfStudyCompleted]; - - var expectedInstances = new List - { - new InstanceFileState - { - Version = 1 - }, - new InstanceFileState - { - Version = 2 - } - }; - - var expectedInstancesWithNewWatermark = new List - { - new InstanceFileState - { - Version = 1, - NewVersion = 3, - }, - new InstanceFileState - { - Version = 2, - NewVersion = 4, - } - }; - - // Arrange the input - string operationId = OperationId.Generate(); - IDurableOrchestrationContext context = CreateContext(operationId); - - List instanceMetadataList = CreateExpectedInstanceMetadataList(expectedInstancesWithNewWatermark, studyInstanceUid); - - context - .GetInput() - .Returns(expectedInput); - context - .CallActivityWithRetryAsync>( - nameof(UpdateDurableFunction.UpdateInstanceWatermarkV2Async), - _options.RetryOptions, - Arg.Any()) - .Returns(instanceMetadataList); - context - .CallActivityWithRetryAsync( - nameof(UpdateDurableFunction.UpdateInstanceBlobsV3Async), - _options.RetryOptions, - Arg.Is(GetPredicate(expectedInput.Partition, instanceMetadataList, expectedInput.ChangeDataset)) - ) - .Returns(new UpdateInstanceResponse(instanceMetadataList, new List())); - context - .CallActivityWithRetryAsync( - nameof(UpdateDurableFunction.CompleteUpdateStudyV4Async), - _options.RetryOptions, - Arg.Is(GetPredicate(expectedInput.Partition.Key, studyInstanceUid, expectedInput.ChangeDataset, instanceMetadataList))) - .Returns(Task.CompletedTask); - context - .CallActivityWithRetryAsync( - nameof(UpdateDurableFunction.DeleteOldVersionBlobV3Async), - _options.RetryOptions, - expectedInstances) - .Returns(Task.CompletedTask); - context - .CallActivityWithRetryAsync( - nameof(UpdateDurableFunction.SetOriginalBlobToColdAccessTierV2Async), - _options.RetryOptions, - Arg.Any()) - .Returns(Task.CompletedTask); - - // Invoke the orchestration - await _updateDurableFunctionWithExternalStore.UpdateInstancesV6Async(context, NullLogger.Instance); - - // Assert behavior - context - .Received(1) - .GetInput(); - await context - .Received(1) - .CallActivityWithRetryAsync>( - nameof(UpdateDurableFunction.UpdateInstanceWatermarkV2Async), - _options.RetryOptions, - Arg.Any()); - await context - .Received(1) - .CallActivityWithRetryAsync( - nameof(UpdateDurableFunction.UpdateInstanceBlobsV3Async), - _options.RetryOptions, - Arg.Is(GetPredicate(expectedInput.Partition, instanceMetadataList, expectedInput.ChangeDataset)) - ); - await context - .Received(1) - .CallActivityWithRetryAsync( - nameof(UpdateDurableFunction.CompleteUpdateStudyV4Async), - _options.RetryOptions, - Arg.Is(GetPredicate(expectedInput.Partition.Key, studyInstanceUid, expectedInput.ChangeDataset, instanceMetadataList))); - context - .Received(1) - .ContinueAsNew( - Arg.Is(x => x.NumberOfStudyCompleted == 1), - false); - await context - .Received(1) - .CallActivityWithRetryAsync( - nameof(UpdateDurableFunction.DeleteOldVersionBlobV3Async), - _options.RetryOptions, - Arg.Any()); - await context - .Received(1) - .CallActivityWithRetryAsync( - nameof(UpdateDurableFunction.SetOriginalBlobToColdAccessTierV2Async), - _options.RetryOptions, - Arg.Any()); - } - - [Fact] - public async Task GivenV4OrchestrationWithInputAndExternalStoreNotEnabled_WhenUpdatingInstances_ThenEmptyInstanceMetadataListPassedInToCompleteUpdate() - { - const int batchSize = 5; - _options.BatchSize = batchSize; - - var expectedInput = GetUpdateCheckpoint(); - var studyInstanceUid = expectedInput.StudyInstanceUids[expectedInput.NumberOfStudyCompleted]; - - var expectedInstances = new List - { - new InstanceFileState - { - Version = 1 - }, - new InstanceFileState - { - Version = 2 - } - }; - - var expectedInstancesWithNewWatermark = new List - { - new InstanceFileState - { - Version = 1, - NewVersion = 3, - }, - new InstanceFileState - { - Version = 2, - NewVersion = 4, - } - }; - - // Arrange the input - string operationId = OperationId.Generate(); - IDurableOrchestrationContext context = CreateContext(operationId); - - List instanceMetadataList = CreateExpectedInstanceMetadataList(expectedInstancesWithNewWatermark); - - context - .GetInput() - .Returns(expectedInput); - context - .CallActivityWithRetryAsync>( - nameof(UpdateDurableFunction.UpdateInstanceWatermarkV2Async), - _options.RetryOptions, - Arg.Any()) - .Returns(instanceMetadataList); - context - .CallActivityWithRetryAsync( - nameof(UpdateDurableFunction.UpdateInstanceBlobsV3Async), - _options.RetryOptions, - Arg.Is(GetPredicate(expectedInput.Partition, instanceMetadataList, expectedInput.ChangeDataset)) - ) - .Returns(new UpdateInstanceResponse(instanceMetadataList, new List())); - context - .CallActivityWithRetryAsync( - nameof(UpdateDurableFunction.CompleteUpdateStudyV4Async), - _options.RetryOptions, - Arg.Is(GetPredicate(expectedInput.Partition.Key, studyInstanceUid, expectedInput.ChangeDataset, new List()))) - .Returns(Task.CompletedTask); - context - .CallActivityWithRetryAsync( - nameof(UpdateDurableFunction.DeleteOldVersionBlobV3Async), - _options.RetryOptions, - expectedInstances) - .Returns(Task.CompletedTask); - context - .CallActivityWithRetryAsync( - nameof(UpdateDurableFunction.SetOriginalBlobToColdAccessTierV2Async), - _options.RetryOptions, - Arg.Any()) - .Returns(Task.CompletedTask); - - // Invoke the orchestration - await _updateDurableFunction.UpdateInstancesV6Async(context, NullLogger.Instance); - - // Assert behavior - context - .Received(1) - .GetInput(); - await context - .Received(1) - .CallActivityWithRetryAsync>( - nameof(UpdateDurableFunction.UpdateInstanceWatermarkV2Async), - _options.RetryOptions, - Arg.Any()); - await context - .Received(1) - .CallActivityWithRetryAsync( - nameof(UpdateDurableFunction.UpdateInstanceBlobsV3Async), - _options.RetryOptions, - Arg.Is(GetPredicate(expectedInput.Partition, instanceMetadataList, expectedInput.ChangeDataset)) - ); - await context - .Received(1) - .CallActivityWithRetryAsync( - nameof(UpdateDurableFunction.CompleteUpdateStudyV4Async), - _options.RetryOptions, - Arg.Is(GetPredicate(expectedInput.Partition.Key, studyInstanceUid, expectedInput.ChangeDataset, null, expectEmptyList: true))); - context - .Received(1) - .ContinueAsNew( - Arg.Is(x => x.NumberOfStudyCompleted == 1), - false); - } - - - [Fact] - public async Task GivenV4OrchestrationWithNoInstancesFound_WhenUpdatingInstances_ThenComplete() - { - const int batchSize = 5; - _options.BatchSize = batchSize; - - DateTime createdTime = DateTime.UtcNow; - - var expectedInput = new UpdateCheckpoint - { - Partition = Partition.Default, - ChangeDataset = string.Empty, - StudyInstanceUids = new List { - TestUidGenerator.Generate() - }, - CreatedTime = createdTime, - }; - - var expectedInstances = new List(); - - var expectedInstancesWithNewWatermark = new List(); - - // Arrange the input - string operationId = OperationId.Generate(); - IDurableOrchestrationContext context = CreateContext(operationId); - - List instanceMetadataList = CreateExpectedInstanceMetadataList(expectedInstancesWithNewWatermark); - - context - .GetInput() - .Returns(expectedInput); - context - .CallActivityWithRetryAsync>( - nameof(UpdateDurableFunction.UpdateInstanceWatermarkV2Async), - _options.RetryOptions, - Arg.Any()) - .Returns(instanceMetadataList); - context - .CallActivityWithRetryAsync( - nameof(UpdateDurableFunction.UpdateInstanceBlobsV3Async), - _options.RetryOptions, - Arg.Is(GetPredicate(expectedInput.Partition, instanceMetadataList, expectedInput.ChangeDataset)) - ) - .Returns(new UpdateInstanceResponse(instanceMetadataList, new List())); - context - .CallActivityWithRetryAsync( - nameof(UpdateDurableFunction.CompleteUpdateStudyV4Async), - _options.RetryOptions, - Arg.Any()) - .Returns(Task.CompletedTask); - context - .CallActivityWithRetryAsync( - nameof(UpdateDurableFunction.DeleteOldVersionBlobV3Async), - _options.RetryOptions, - expectedInstances) - .Returns(Task.CompletedTask); - context - .CallActivityWithRetryAsync( - nameof(UpdateDurableFunction.SetOriginalBlobToColdAccessTierV2Async), - _options.RetryOptions, - Arg.Any()) - .Returns(Task.CompletedTask); - - // Invoke the orchestration - await _updateDurableFunction.UpdateInstancesV6Async(context, NullLogger.Instance); - - // Assert behavior - context - .Received(1) - .GetInput(); - await context - .Received(1) - .CallActivityWithRetryAsync>( - nameof(UpdateDurableFunction.UpdateInstanceWatermarkV2Async), - _options.RetryOptions, - Arg.Any()); - await context - .DidNotReceive() - .CallActivityWithRetryAsync( - nameof(UpdateDurableFunction.UpdateInstanceBlobsV3Async), - _options.RetryOptions, - Arg.Is(GetPredicate(Partition.Default, instanceMetadataList, expectedInput.ChangeDataset))); - await context - .DidNotReceive() - .CallActivityWithRetryAsync( - nameof(UpdateDurableFunction.CompleteUpdateStudyV4Async), - _options.RetryOptions, - Arg.Any()); - context - .Received(1) - .ContinueAsNew( - Arg.Any(), - false); - await context - .DidNotReceive() - .CallActivityWithRetryAsync( - nameof(UpdateDurableFunction.DeleteOldVersionBlobV3Async), - _options.RetryOptions, - Arg.Any()); - await context - .DidNotReceive() - .CallActivityWithRetryAsync( - nameof(UpdateDurableFunction.SetOriginalBlobToColdAccessTierV2Async), - _options.RetryOptions, - Arg.Any()); - - _meterProvider.ForceFlush(); - Assert.Empty(_exportedItems.Where(item => item.Name.Equals(_updateMeter.UpdatedInstances.Name, StringComparison.Ordinal))); - } - - - [Fact] - public async Task GivenV4OrchestrationWithInput_WhenUpdatingInstancesWithException_ThenFails() - { - const int batchSize = 5; - _options.BatchSize = batchSize; - - DateTime createdTime = DateTime.UtcNow; - - var expectedInput = new UpdateCheckpoint - { - Partition = Partition.Default, - ChangeDataset = string.Empty, - StudyInstanceUids = new List(), - CreatedTime = createdTime, - Errors = new List() - { - "Failed Study" - } - }; - - // Arrange the input - string operationId = OperationId.Generate(); - IDurableOrchestrationContext context = CreateContext(operationId); - - context - .GetInput() - .Returns(expectedInput); - - // Invoke the orchestration - await Assert.ThrowsAsync(() => _updateDurableFunction.UpdateInstancesV6Async(context, NullLogger.Instance)); - - // Assert behavior - context - .Received(1) - .GetInput(); - await context - .DidNotReceive() - .CallActivityWithRetryAsync>( - nameof(UpdateDurableFunction.UpdateInstanceWatermarkV2Async), - _options.RetryOptions, - Arg.Any()); - await context - .DidNotReceive() - .CallActivityWithRetryAsync( - nameof(UpdateDurableFunction.UpdateInstanceBlobsV3Async), - _options.RetryOptions, - Arg.Any()); - await context - .DidNotReceive() - .CallActivityWithRetryAsync( - nameof(UpdateDurableFunction.CompleteUpdateStudyV4Async), - _options.RetryOptions, - Arg.Any()); - context - .DidNotReceive() - .ContinueAsNew( - Arg.Any(), - false); - - _meterProvider.ForceFlush(); - Assert.Empty(_exportedItems.Where(item => item.Name.Equals(_updateMeter.UpdatedInstances.Name, StringComparison.Ordinal))); - } - - [Fact] - public async Task GivenV4OrchestrationWithInput_WhenUpdatingInstancesWithException_ThenCallCleanupActivity() - { - const int batchSize = 5; - _options.BatchSize = batchSize; - - DateTime createdTime = DateTime.UtcNow; - - List instanceMetadataList = new List { - new InstanceMetadata( - new VersionedInstanceIdentifier( - TestUidGenerator.Generate(), - TestUidGenerator.Generate(), - TestUidGenerator.Generate(), - version: 1, - Partition.Default), - new InstanceProperties - { - FileProperties = new FileProperties { ETag = $"etag-{1}", Path = $"path-{1}" , ContentLength = 123}, - NewVersion = 3 - } - ), - new InstanceMetadata( - new VersionedInstanceIdentifier( - TestUidGenerator.Generate(), - TestUidGenerator.Generate(), - TestUidGenerator.Generate(), - version: 2, - Partition.Default), - new InstanceProperties - { - FileProperties = new FileProperties { ETag = $"etag-{2}", Path = $"path-{2}", ContentLength = 456}, - NewVersion = 4 - } - ) - }; - - var expectedInstancesWithNewWatermark = instanceMetadataList.Select(x => x.ToInstanceFileState()).ToList(); - - var expectedInput = new UpdateCheckpoint - { - Partition = Partition.Default, - ChangeDataset = string.Empty, - StudyInstanceUids = instanceMetadataList.Select(x => x.VersionedInstanceIdentifier.StudyInstanceUid).ToList(), - CreatedTime = createdTime, - }; - - // Arrange the input - string operationId = OperationId.Generate(); - IDurableOrchestrationContext context = CreateContext(operationId); - - context - .GetInput() - .Returns(expectedInput); - - context - .CallActivityWithRetryAsync>( - nameof(UpdateDurableFunction.UpdateInstanceWatermarkV2Async), - _options.RetryOptions, Arg.Any()).Returns(instanceMetadataList); - - context - .CallActivityWithRetryAsync( - nameof(UpdateDurableFunction.UpdateInstanceBlobsV3Async), - _options.RetryOptions, - Arg.Is(GetPredicate(Partition.Default, instanceMetadataList, expectedInput.ChangeDataset))) - .ThrowsAsync(new FunctionFailedException("Function failed")); - - context - .CallActivityWithRetryAsync( - nameof(UpdateDurableFunction.CleanupNewVersionBlobV3Async), - _options.RetryOptions, - expectedInstancesWithNewWatermark) - .Returns(Task.CompletedTask); - - // Invoke the orchestration - await _updateDurableFunction.UpdateInstancesV6Async(context, NullLogger.Instance); - - // Assert behavior - await context - .Received(1) - .CallActivityWithRetryAsync( - nameof(UpdateDurableFunction.CleanupNewVersionBlobV3Async), - _options.RetryOptions, - Arg.Is(GetPredicate(instanceMetadataList, Partition.Default))); - - _meterProvider.ForceFlush(); - Assert.Empty(_exportedItems.Where(item => item.Name.Equals(_updateMeter.UpdatedInstances.Name, StringComparison.Ordinal))); - } - - [Fact] - public async Task GivenV4OrchestrationWithInput_WhenUpdatingInstancesWithDataStoreFailure_ThenCallCleanupActivity() - { - const int batchSize = 5; - _options.BatchSize = batchSize; - - DateTime createdTime = DateTime.UtcNow; - - List instanceMetadataList = new List { - new InstanceMetadata( - new VersionedInstanceIdentifier( - TestUidGenerator.Generate(), - TestUidGenerator.Generate(), - TestUidGenerator.Generate(), - version: 1, - Partition.Default), - new InstanceProperties - { - FileProperties = new FileProperties { ETag = $"etag-{1}", Path = $"path-{1}", ContentLength = 123}, - NewVersion = 3 - } - ), - new InstanceMetadata( - new VersionedInstanceIdentifier( - TestUidGenerator.Generate(), - TestUidGenerator.Generate(), - TestUidGenerator.Generate(), - version: 2, - Partition.Default), - new InstanceProperties - { - FileProperties = new FileProperties { ETag = $"etag-{2}", Path = $"path-{2}", ContentLength = 456}, - NewVersion = 4 - } - ) - }; - - var expectedInstancesWithNewWatermark = instanceMetadataList.Select(x => x.ToInstanceFileState()).ToList(); - - var expectedInput = new UpdateCheckpoint - { - Partition = Partition.Default, - ChangeDataset = string.Empty, - StudyInstanceUids = instanceMetadataList.Select(x => x.VersionedInstanceIdentifier.StudyInstanceUid).ToList(), - CreatedTime = createdTime, - }; - - // Arrange the input - string operationId = OperationId.Generate(); - IDurableOrchestrationContext context = CreateContext(operationId); - - context - .GetInput() - .Returns(expectedInput); - - context - .CallActivityWithRetryAsync>( - nameof(UpdateDurableFunction.UpdateInstanceWatermarkV2Async), - _options.RetryOptions, Arg.Any()).Returns(instanceMetadataList); - - context - .CallActivityWithRetryAsync( - nameof(UpdateDurableFunction.UpdateInstanceBlobsV3Async), - _options.RetryOptions, - Arg.Is(GetPredicate(Partition.Default, instanceMetadataList, expectedInput.ChangeDataset))) - .Returns(new UpdateInstanceResponse(instanceMetadataList, new List { "Instance Error" })); - - context - .CallActivityWithRetryAsync( - nameof(UpdateDurableFunction.CleanupNewVersionBlobV3Async), - _options.RetryOptions, - expectedInstancesWithNewWatermark) - .Returns(Task.CompletedTask); - - // Invoke the orchestration - await _updateDurableFunction.UpdateInstancesV6Async(context, NullLogger.Instance); - - // Assert behavior - await context - .Received(1) - .CallActivityWithRetryAsync( - nameof(UpdateDurableFunction.CleanupNewVersionBlobV3Async), - _options.RetryOptions, - Arg.Is(GetPredicate(instanceMetadataList, Partition.Default))); - - _meterProvider.ForceFlush(); - Assert.Empty(_exportedItems.Where(item => item.Name.Equals(_updateMeter.UpdatedInstances.Name, StringComparison.Ordinal))); - } - - [Fact] - public async Task GivenV4OrchestrationWithInput_WhenUpdatingInstances_ThenCompleteWithUpdateProgress() - { - const int batchSize = 5; - _options.BatchSize = batchSize; - - DateTime createdTime = DateTime.UtcNow; - - var expectedInput = GetUpdateCheckpoint(); - - var expectedInstances = new List - { - new InstanceFileState - { - Version = 1 - }, - new InstanceFileState - { - Version = 2 - } - }; - - var expectedInstancesWithNewWatermark = new List - { - new InstanceFileState - { - Version = 1, - NewVersion = 3, - }, - new InstanceFileState - { - Version = 2, - NewVersion = 4, - } - }; - - List instanceMetadataList = CreateExpectedInstanceMetadataList(expectedInstancesWithNewWatermark); - - // Arrange the input - string operationId = OperationId.Generate(); - IDurableOrchestrationContext context = CreateContext(operationId); - - context - .GetInput() - .Returns(expectedInput); - context - .CallActivityWithRetryAsync>( - nameof(UpdateDurableFunction.UpdateInstanceWatermarkV2Async), - _options.RetryOptions, - Arg.Any()) - .Returns(instanceMetadataList); - context - .CallActivityWithRetryAsync( - nameof(UpdateDurableFunction.UpdateInstanceBlobsV3Async), - _options.RetryOptions, - Arg.Is(GetPredicate(expectedInput.Partition, instanceMetadataList, expectedInput.ChangeDataset)) - ) - .Returns(new UpdateInstanceResponse(instanceMetadataList, new List())); - context - .CallActivityWithRetryAsync( - nameof(UpdateDurableFunction.CompleteUpdateStudyV4Async), - _options.RetryOptions, - Arg.Any()) - .Returns(Task.CompletedTask); - context - .CallActivityWithRetryAsync( - nameof(UpdateDurableFunction.DeleteOldVersionBlobV3Async), - _options.RetryOptions, - expectedInstances) - .Returns(Task.CompletedTask); - - // Invoke the orchestration - await _updateDurableFunction.UpdateInstancesV6Async(context, NullLogger.Instance); - - // Assert behavior - context - .Received(1) - .GetInput(); - await context - .Received(1) - .CallActivityWithRetryAsync>( - nameof(UpdateDurableFunction.UpdateInstanceWatermarkV2Async), - _options.RetryOptions, - Arg.Any()); - await context - .Received(1) - .CallActivityWithRetryAsync( - nameof(UpdateDurableFunction.UpdateInstanceBlobsV3Async), - _options.RetryOptions, - Arg.Is(GetPredicate(Partition.Default, instanceMetadataList, expectedInput.ChangeDataset))); - await context - .Received(1) - .CallActivityWithRetryAsync( - nameof(UpdateDurableFunction.CompleteUpdateStudyV4Async), - _options.RetryOptions, - Arg.Any()); - context - .Received(1) - .ContinueAsNew( - Arg.Is(GetPredicate(expectedInstancesWithNewWatermark.Count, 1)), - false); - } - - private static IDurableOrchestrationContext CreateContext() - => CreateContext(OperationId.Generate()); - - private static UpdateCheckpoint GetUpdateCheckpoint() - => new UpdateCheckpoint - { - Partition = Partition.Default, - ChangeDataset = string.Empty, - StudyInstanceUids = new List { - TestUidGenerator.Generate(), - TestUidGenerator.Generate(), - TestUidGenerator.Generate() - }, - CreatedTime = DateTime.UtcNow - }; - - private static IDurableOrchestrationContext CreateContext(string operationId) - { - IDurableOrchestrationContext context = Substitute.For(); - context.InstanceId.Returns(operationId); - return context; - } - - private static Expression> GetPredicate(Partition partition, IReadOnlyList instanceMetadataList, string changeDataset) - { - return x => - x.InstanceMetadataList == instanceMetadataList - && x.ChangeDataset == changeDataset - && x.Partition == partition; - } - - private static Expression> GetPredicate(InstanceMetadata instance) - { - return x => - x.InstanceProperties.NewVersion == instance.InstanceProperties.NewVersion - && x.VersionedInstanceIdentifier.Version == instance.VersionedInstanceIdentifier.Version - && x.InstanceProperties.OriginalVersion == instance.InstanceProperties.OriginalVersion; - } - - private static Expression> GetPredicate(int partitionKey, string studyInstanceUid, string dicomDataset, IReadOnlyList instanceMetadataList, bool expectEmptyList = false) - { - return x => - x.PartitionKey == partitionKey - && x.StudyInstanceUid == studyInstanceUid - && x.ChangeDataset == dicomDataset - && expectEmptyList ? x.InstanceMetadataList.IsNullOrEmpty() : x.InstanceMetadataList == instanceMetadataList; - } - - private static Expression> GetPredicate(IReadOnlyList instances, Partition partition) - { - return x => x.Instances.IsNullOrEmpty() == false - && x.Instances[0].VersionedInstanceIdentifier.Version == instances[0].VersionedInstanceIdentifier.Version - && x.Instances[1].VersionedInstanceIdentifier.Version == instances[1].VersionedInstanceIdentifier.Version - && x.Partition == partition; - } - private static Expression> GetPredicate(long instanceUpdated, int studyCompleted) - { - return r => r.TotalNumberOfInstanceUpdated == instanceUpdated - && r.NumberOfStudyCompleted == studyCompleted; - } - - private static List CreateExpectedInstanceMetadataList(List expectedInstancesWithNewWatermark, string studyInstanceUid = "0") - { - List instanceMetadataList = expectedInstancesWithNewWatermark.Select(x => new InstanceMetadata(new VersionedInstanceIdentifier(studyInstanceUid, "0", "0", x.Version), new InstanceProperties - { - FileProperties = new FileProperties - { - ETag = $"etag-{x.NewVersion}", - Path = $"path-{x.NewVersion}", - } - })).ToList(); - return instanceMetadataList; - } - -} diff --git a/src/Microsoft.Health.Dicom.Functions.UnitTests/Update/UpdateDurableFunctionTests.cs b/src/Microsoft.Health.Dicom.Functions.UnitTests/Update/UpdateDurableFunctionTests.cs deleted file mode 100644 index b3f1a183b1..0000000000 --- a/src/Microsoft.Health.Dicom.Functions.UnitTests/Update/UpdateDurableFunctionTests.cs +++ /dev/null @@ -1,99 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Text.Json; -using FellowOakDicom.Serialization; -using Microsoft.ApplicationInsights; -using Microsoft.ApplicationInsights.Channel; -using Microsoft.ApplicationInsights.Extensibility; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Features.Audit; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Features.Retrieve; -using Microsoft.Health.Dicom.Core.Features.Store; -using Microsoft.Health.Dicom.Core.Features.Telemetry; -using Microsoft.Health.Dicom.Core.Features.Update; -using Microsoft.Health.Dicom.Core.Serialization; -using Microsoft.Health.Dicom.Functions.Update; -using Microsoft.Health.Operations.Functions.DurableTask; -using NSubstitute; -using OpenTelemetry; -using OpenTelemetry.Metrics; -using Metric = OpenTelemetry.Metrics.Metric; - -namespace Microsoft.Health.Dicom.Functions.UnitTests.Update; - -public partial class UpdateDurableFunctionTests -{ - private readonly UpdateDurableFunction _updateDurableFunction; - private readonly UpdateDurableFunction _updateDurableFunctionWithExternalStore; - private readonly IIndexDataStore _indexStore; - private readonly IInstanceStore _instanceStore; - private readonly UpdateOptions _options; - private readonly IMetadataStore _metadataStore; - private readonly IFileStore _fileStore; - private readonly IUpdateInstanceService _updateInstanceService; - private readonly UpdateMeter _updateMeter; - private readonly JsonSerializerOptions _jsonSerializerOptions; - private MeterProvider _meterProvider; - private List _exportedItems; - - public UpdateDurableFunctionTests() - { - _indexStore = Substitute.For(); - _instanceStore = Substitute.For(); - _metadataStore = Substitute.For(); - _fileStore = Substitute.For(); - _updateInstanceService = Substitute.For(); - _options = new UpdateOptions { RetryOptions = new ActivityRetryOptions() }; - _jsonSerializerOptions = new JsonSerializerOptions(); - _jsonSerializerOptions.Converters.Add(new DicomJsonConverter(writeTagsAsKeywords: true, autoValidate: false, numberSerializationMode: NumberSerializationMode.PreferablyAsNumber)); - _updateMeter = new UpdateMeter(); - _jsonSerializerOptions.Converters.Add(new ExportDataOptionsJsonConverter()); - var telemetryClient = new TelemetryClient(new TelemetryConfiguration() - { - TelemetryChannel = Substitute.For(), - }); - _updateDurableFunction = new UpdateDurableFunction( - _indexStore, - _instanceStore, - Options.Create(_options), - _metadataStore, - _fileStore, - _updateInstanceService, - Substitute.For(), - _updateMeter, - telemetryClient, - Substitute.For(), - Options.Create(_jsonSerializerOptions), - Options.Create(new FeatureConfiguration())); - _updateDurableFunctionWithExternalStore = new UpdateDurableFunction( - _indexStore, - _instanceStore, - Options.Create(_options), - _metadataStore, - _fileStore, - _updateInstanceService, - Substitute.For(), - _updateMeter, - telemetryClient, - Substitute.For(), - Options.Create(_jsonSerializerOptions), - Options.Create(new FeatureConfiguration { EnableExternalStore = true, })); - InitializeMetricExporter(); - } - - private void InitializeMetricExporter() - { - _exportedItems = new List(); - _meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter($"{OpenTelemetryLabels.BaseMeterName}.Update") - .AddInMemoryExporter(_exportedItems) - .Build(); - } -} diff --git a/src/Microsoft.Health.Dicom.Functions/Configuration/DicomFunctionsConfiguration.cs b/src/Microsoft.Health.Dicom.Functions/Configuration/DicomFunctionsConfiguration.cs deleted file mode 100644 index 5dc0580995..0000000000 --- a/src/Microsoft.Health.Dicom.Functions/Configuration/DicomFunctionsConfiguration.cs +++ /dev/null @@ -1,11 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -namespace Microsoft.Health.Dicom.Functions.Configuration; - -internal static class DicomFunctionsConfiguration -{ - public const string SectionName = "DicomFunctions"; -} diff --git a/src/Microsoft.Health.Dicom.Functions/ContentLengthBackFill/ContentLengthBackFillDurableFunction.cs b/src/Microsoft.Health.Dicom.Functions/ContentLengthBackFill/ContentLengthBackFillDurableFunction.cs deleted file mode 100644 index c3e15c5ad9..0000000000 --- a/src/Microsoft.Health.Dicom.Functions/ContentLengthBackFill/ContentLengthBackFillDurableFunction.cs +++ /dev/null @@ -1,32 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Retrieve; -using Microsoft.Health.Dicom.Core.Features.Store; - -namespace Microsoft.Health.Dicom.Functions.ContentLengthBackFill; - -public partial class ContentLengthBackFillDurableFunction -{ - private readonly IInstanceStore _instanceStore; - private readonly IIndexDataStore _indexDataStore; - private readonly IFileStore _fileStore; - private readonly ContentLengthBackFillOptions _options; - - public ContentLengthBackFillDurableFunction( - IInstanceStore instanceStore, - IIndexDataStore indexDataStore, - IFileStore fileStore, - IOptions configOptions) - { - _instanceStore = EnsureArg.IsNotNull(instanceStore, nameof(instanceStore)); - _indexDataStore = EnsureArg.IsNotNull(indexDataStore, nameof(indexDataStore)); - _fileStore = EnsureArg.IsNotNull(fileStore, nameof(fileStore)); - _options = EnsureArg.IsNotNull(configOptions?.Value, nameof(configOptions)); - } -} diff --git a/src/Microsoft.Health.Dicom.Functions/ContentLengthBackFill/ContentLengthBackfillDurableFunction.Activity.cs b/src/Microsoft.Health.Dicom.Functions/ContentLengthBackFill/ContentLengthBackfillDurableFunction.Activity.cs deleted file mode 100644 index 839e757e04..0000000000 --- a/src/Microsoft.Health.Dicom.Functions/ContentLengthBackFill/ContentLengthBackfillDurableFunction.Activity.cs +++ /dev/null @@ -1,137 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Azure.Storage.Blobs.Models; -using EnsureThat; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Microsoft.Extensions.Logging; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Functions.ContentLengthBackFill.Models; - -namespace Microsoft.Health.Dicom.Functions.ContentLengthBackFill; - -public partial class ContentLengthBackFillDurableFunction -{ - internal static int CorruptedAndProcessed = -1; - private const int ExpectedMinValue = 1; - - /// - /// Asynchronously retrieves the next set of instance batches based on the configured options and whatever - /// instances meet criteria of needing content length backfilled on file properties - /// - /// The options for configuring the batches. - /// A diagnostic logger. - /// - /// A task representing the asynchronous get operation. The value of its - /// property contains a list of batches as defined by their smallest and largest watermark. - /// - /// - /// or is . - /// - [FunctionName(nameof(GetContentLengthBackFillInstanceBatches))] - public Task> GetContentLengthBackFillInstanceBatches([ActivityTrigger] BatchCreationArguments arguments, ILogger logger) - { - EnsureArg.IsNotNull(arguments, nameof(arguments)); - EnsureArg.IsNotNull(logger, nameof(logger)); - - logger.LogInformation("Dividing up the instances into batches starting from the end."); - - return _instanceStore.GetContentLengthBackFillInstanceBatches( - arguments.BatchSize, - arguments.MaxParallelBatches, - CancellationToken.None); - } - - /// - /// Asynchronously retrieve content length from blob store and update content length on file Properties for the - /// instances in the given watermark range. - /// - /// The options that include the instances to clean up - /// A diagnostic logger. - /// A task representing the operation. - /// - /// is . - /// - [FunctionName(nameof(BackFillContentLengthRangeDataAsync))] - public async Task BackFillContentLengthRangeDataAsync([ActivityTrigger] WatermarkRange watermarkRange, ILogger logger) - { - EnsureArg.IsNotNull(logger, nameof(logger)); - - IReadOnlyList instanceIdentifiers = - await _instanceStore.GetContentLengthBackFillInstanceIdentifiersByWatermarkRangeAsync(watermarkRange); - - logger.LogInformation("Getting content length for the instances in the range {Range}.", watermarkRange); - - var propertiesByWatermark = new ConcurrentDictionary(); - await Parallel.ForEachAsync( - instanceIdentifiers, - new ParallelOptions - { - CancellationToken = default, - MaxDegreeOfParallelism = _options.MaxParallelThreads, - }, - async (instanceIdentifier, token) => - { - FileProperties blobStoreFileProperties; - try - { - blobStoreFileProperties = await _fileStore.GetFilePropertiesAsync( - instanceIdentifier.Version, - instanceIdentifier.Partition, - fileProperties: null, token); - if (blobStoreFileProperties.ContentLength < ExpectedMinValue) - { - blobStoreFileProperties = new FileProperties { ContentLength = CorruptedAndProcessed }; - logger.LogWarning( - "Content length for the instance with watermark {Watermark} in partition {Partition} appears to be corrupted. Value should be {ExpectedMin} or greater, but it was {Length}. Will store as {Value} to mark as processed.", - instanceIdentifier.Version, - instanceIdentifier.Partition.Key, - ExpectedMinValue, - blobStoreFileProperties.ContentLength, - CorruptedAndProcessed); - } - propertiesByWatermark.TryAdd(instanceIdentifier.Version, blobStoreFileProperties); - } - catch (Exception e) when (e is DataStoreException or DataStoreRequestFailedException) - { - if (e is DataStoreRequestFailedException && - e.Message.Contains(BlobErrorCode.ConditionNotMet.ToString(), StringComparison.InvariantCulture)) - { - propertiesByWatermark.TryAdd(instanceIdentifier.Version, new FileProperties { ContentLength = CorruptedAndProcessed }); - logger.LogWarning( - "Could not get content length from blob store for the instance with watermark {Watermark} in partition {Partition} due to data corruption. The file may be missing or etags mismatch. Will store as {Value} to mark as processed.", - instanceIdentifier.Version, - instanceIdentifier.Partition.Key, - CorruptedAndProcessed); - } - else - { - // try to reprocess later by leaving content length 0, but allow other instances to attempt to update - logger.LogInformation( - "Could not get content length from blob store for the instance with watermark {Watermark} in partition {Partition}. Will leave length as 0 to allow for reprocessing as data does not appear to be corrupted.", - instanceIdentifier.Version, - instanceIdentifier.Partition.Key); - } - } - }); - - logger.LogInformation("Completed getting content length for the instances in the range {Range}.", watermarkRange); - - await _indexDataStore.UpdateFilePropertiesContentLengthAsync(propertiesByWatermark); - - logger.LogInformation( - "Complete updating content length for the instances in the range {Range}, with total instances updated count of {TotalInstanceUpdated}.", - watermarkRange, - propertiesByWatermark.Count); - } -} diff --git a/src/Microsoft.Health.Dicom.Functions/ContentLengthBackFill/ContentLengthBackfillDurableFunction.Orchestration.cs b/src/Microsoft.Health.Dicom.Functions/ContentLengthBackFill/ContentLengthBackfillDurableFunction.Orchestration.cs deleted file mode 100644 index 700050b716..0000000000 --- a/src/Microsoft.Health.Dicom.Functions/ContentLengthBackFill/ContentLengthBackfillDurableFunction.Orchestration.cs +++ /dev/null @@ -1,83 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Microsoft.Extensions.Logging; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Functions.ContentLengthBackFill.Models; -using Microsoft.Health.Operations.Functions.DurableTask; - -namespace Microsoft.Health.Dicom.Functions.ContentLengthBackFill; - -public partial class ContentLengthBackFillDurableFunction -{ - /// - /// Asynchronously backfills the content length for instance data. - /// - /// - /// Durable functions are reliable, and their implementations will be executed repeatedly over the lifetime of - /// a single instance. - /// - /// The context for the orchestration instance. - /// A diagnostic logger. - /// A task representing the operation. - /// - /// or is . - /// - /// Orchestration instance ID is invalid. - [FunctionName(nameof(ContentLengthBackFillAsync))] - public async Task ContentLengthBackFillAsync( - [OrchestrationTrigger] IDurableOrchestrationContext context, - ILogger logger) - { - EnsureArg.IsNotNull(context, nameof(context)).ThrowIfInvalidOperationId(); - logger = context.CreateReplaySafeLogger(EnsureArg.IsNotNull(logger, nameof(logger))); - - ContentLengthBackFillCheckPoint input = context.GetInput(); - - IReadOnlyList batches = await context.CallActivityWithRetryAsync>( - nameof(GetContentLengthBackFillInstanceBatches), - _options.RetryOptions, - new BatchCreationArguments(input.Batching.Size, input.Batching.MaxParallelCount)); - - if (batches.Count > 0) - { - // Batches are in reverse order because we start from the highest watermark - var batchRange = new WatermarkRange(batches[^1].Start, batches[0].End); - - logger.LogInformation("Beginning to back fill content length data range {Range}.", batchRange); - await Task.WhenAll(batches - .Select(x => context.CallActivityWithRetryAsync( - nameof(BackFillContentLengthRangeDataAsync), - _options.RetryOptions, - x))); - - // Create a new orchestration with the same instance ID to process the remaining data - logger.LogInformation("Completed back fill for content length data in the range {Range}. Continuing with new execution...", batchRange); - - WatermarkRange completed = input.Completed.HasValue - ? new WatermarkRange(batchRange.Start, input.Completed.Value.End) - : batchRange; - - context.ContinueAsNew( - new ContentLengthBackFillCheckPoint() - { - Batching = input.Batching, - Completed = completed, - CreatedTime = input.CreatedTime ?? await context.GetCreatedTimeAsync(_options.RetryOptions), - }); - } - else - { - logger.LogInformation("Completed back fill for content length data operation."); - } - } -} diff --git a/src/Microsoft.Health.Dicom.Functions/ContentLengthBackFill/ContentLengthBackfillOptions.cs b/src/Microsoft.Health.Dicom.Functions/ContentLengthBackFill/ContentLengthBackfillOptions.cs deleted file mode 100644 index 8fb8b1a5bb..0000000000 --- a/src/Microsoft.Health.Dicom.Functions/ContentLengthBackFill/ContentLengthBackfillOptions.cs +++ /dev/null @@ -1,26 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.ComponentModel.DataAnnotations; -using Microsoft.Health.Operations.Functions.DurableTask; - -namespace Microsoft.Health.Dicom.Functions.ContentLengthBackFill; - -public class ContentLengthBackFillOptions -{ - internal const string SectionName = "ContentLengthBackFill"; - - /// - /// Gets or sets the number of threads available for each batch. - /// - [Range(-1, int.MaxValue)] - public int MaxParallelThreads { get; set; } = -1; - - /// - /// Gets or sets the for cleanup activities. - /// - public ActivityRetryOptions RetryOptions { get; set; } -} diff --git a/src/Microsoft.Health.Dicom.Functions/ContentLengthBackFill/Models/BatchCreationArguments.cs b/src/Microsoft.Health.Dicom.Functions/ContentLengthBackFill/Models/BatchCreationArguments.cs deleted file mode 100644 index c20abf74dd..0000000000 --- a/src/Microsoft.Health.Dicom.Functions/ContentLengthBackFill/Models/BatchCreationArguments.cs +++ /dev/null @@ -1,41 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using EnsureThat; - -namespace Microsoft.Health.Dicom.Functions.ContentLengthBackFill.Models; - -public class BatchCreationArguments -{ - /// - /// Gets or sets the number of DICOM instances processed by a single activity. - /// - public int BatchSize { get; } - - /// - /// Gets or sets the maximum number of concurrent batches processed at a given time. - /// - public int MaxParallelBatches { get; } - - /// - /// Initializes a new instance of the class with the specified values. - /// - /// The number of DICOM instances processed by a single activity. - /// The maximum number of concurrent batches processed at a given time. - /// - /// is less than 1. - /// -or- - /// is less than 1. - /// - public BatchCreationArguments(int batchSize, int maxParallelBatches) - { - EnsureArg.IsGte(batchSize, 1, nameof(batchSize)); - EnsureArg.IsGte(maxParallelBatches, 1, nameof(maxParallelBatches)); - - BatchSize = batchSize; - MaxParallelBatches = maxParallelBatches; - } -} diff --git a/src/Microsoft.Health.Dicom.Functions/DataCleanup/DataCleanupDurableFunction.Activity.cs b/src/Microsoft.Health.Dicom.Functions/DataCleanup/DataCleanupDurableFunction.Activity.cs deleted file mode 100644 index 84fa23db15..0000000000 --- a/src/Microsoft.Health.Dicom.Functions/DataCleanup/DataCleanupDurableFunction.Activity.cs +++ /dev/null @@ -1,115 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Microsoft.Extensions.Logging; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Models; -using Microsoft.Health.Dicom.Functions.DataCleanup.Models; - -namespace Microsoft.Health.Dicom.Functions.DataCleanup; - -public partial class DataCleanupDurableFunction -{ - /// - /// Asynchronously retrieves the next set of instance batches based on the configured options. - /// - /// The options for configuring the batches. - /// A diagnostic logger. - /// - /// A task representing the asynchronous get operation. The value of its - /// property contains a list of batches as defined by their smallest and largest watermark. - /// - /// - /// or is . - /// - [FunctionName(nameof(GetInstanceBatchesByTimeStampAsync))] - public Task> GetInstanceBatchesByTimeStampAsync( - [ActivityTrigger] DataCleanupBatchCreationArguments arguments, - ILogger logger) - { - EnsureArg.IsNotNull(arguments, nameof(arguments)); - EnsureArg.IsNotNull(logger, nameof(logger)); - - if (arguments.MaxWatermark.HasValue) - { - logger.LogInformation("Dividing up the instances into batches starting from the largest watermark {Watermark}.", arguments.MaxWatermark); - } - else - { - logger.LogInformation("Dividing up the instances into batches starting from the end."); - } - - return _instanceStore.GetInstanceBatchesByTimeStampAsync( - arguments.BatchSize, - arguments.MaxParallelBatches, - IndexStatus.Created, - arguments.StartFilterTimeStamp, - arguments.EndFilterTimeStamp, - arguments.MaxWatermark, - CancellationToken.None); - } - - /// - /// Asynchronously update HasFrameMetadata for the instances in the given watermark range. - /// - /// The options that include the instances to clean up - /// A diagnostic logger. - /// A task representing the operation. - /// - /// is . - /// - [FunctionName(nameof(CleanupFrameRangeDataAsync))] - public async Task CleanupFrameRangeDataAsync([ActivityTrigger] WatermarkRange watermarkRange, ILogger logger) - { - EnsureArg.IsNotNull(logger, nameof(logger)); - - IReadOnlyList instanceIdentifiers = - await _instanceStore.GetInstanceIdentifiersByWatermarkRangeAsync(watermarkRange, IndexStatus.Created); - - logger.LogInformation("Getting isFrameRangeExists for the instances in the range {Range}.", watermarkRange); - - var concurrentDictionary = new ConcurrentDictionary(); - await Parallel.ForEachAsync( - instanceIdentifiers, - new ParallelOptions - { - CancellationToken = default, - MaxDegreeOfParallelism = _options.MaxParallelThreads, - }, - async (instanceIdentifier, token) => - { - bool isFrameRangeExists = await _metadataStore.DoesFrameRangeExistAsync(instanceIdentifier.Version, token); - if (isFrameRangeExists) - concurrentDictionary.TryAdd(instanceIdentifier, isFrameRangeExists); - }); - - logger.LogInformation("Completed getting isFrameRangeExists for the instances in the range {Range}.", watermarkRange); - - var groupByPartitionList = concurrentDictionary.GroupBy(x => x.Key.Partition.Key).ToList(); - - await Parallel.ForEachAsync( - groupByPartitionList, - new ParallelOptions - { - CancellationToken = default, - MaxDegreeOfParallelism = _options.MaxParallelThreads, - }, - (dict, token) => - { - return new ValueTask(_indexDataStore.UpdateFrameDataAsync(dict.Key, dict.Select(x => x.Key.Version).ToList(), hasFrameMetadata: true, token)); - }); - - logger.LogInformation("Completed updating hasFrameMetadata in the range {Range}, {TotalInstanceUpdated}.", watermarkRange, concurrentDictionary.Count); - } -} diff --git a/src/Microsoft.Health.Dicom.Functions/DataCleanup/DataCleanupDurableFunction.Orchestration.cs b/src/Microsoft.Health.Dicom.Functions/DataCleanup/DataCleanupDurableFunction.Orchestration.cs deleted file mode 100644 index 7d0dfb29b6..0000000000 --- a/src/Microsoft.Health.Dicom.Functions/DataCleanup/DataCleanupDurableFunction.Orchestration.cs +++ /dev/null @@ -1,90 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Microsoft.Extensions.Logging; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Functions.DataCleanup.Models; -using Microsoft.Health.Operations.Functions.DurableTask; - -namespace Microsoft.Health.Dicom.Functions.DataCleanup; - -public partial class DataCleanupDurableFunction -{ - /// - /// Asynchronously cleans up instance data. - /// - /// - /// Durable functions are reliable, and their implementations will be executed repeatedly over the lifetime of - /// a single instance. - /// - /// The context for the orchestration instance. - /// A diagnostic logger. - /// A task representing the operation. - /// - /// or is . - /// - /// Orchestration instance ID is invalid. - [FunctionName(nameof(DataCleanupAsync))] - public async Task DataCleanupAsync( - [OrchestrationTrigger] IDurableOrchestrationContext context, - ILogger logger) - { - EnsureArg.IsNotNull(context, nameof(context)).ThrowIfInvalidOperationId(); - logger = context.CreateReplaySafeLogger(EnsureArg.IsNotNull(logger, nameof(logger))); - - DataCleanupCheckPoint input = context.GetInput(); - - IReadOnlyList batches = await context.CallActivityWithRetryAsync>( - nameof(GetInstanceBatchesByTimeStampAsync), - _options.RetryOptions, - new DataCleanupBatchCreationArguments( - input.Completed?.Start - 1, - input.Batching.Size, - input.Batching.MaxParallelCount, - input.StartFilterTimeStamp, - input.EndFilterTimeStamp)); - - if (batches.Count > 0) - { - // Batches are in reverse order because we start from the highest watermark - var batchRange = new WatermarkRange(batches[^1].Start, batches[0].End); - - logger.LogInformation("Beginning to cleanup frame range data {Range}.", batchRange); - await Task.WhenAll(batches - .Select(x => context.CallActivityWithRetryAsync( - nameof(CleanupFrameRangeDataAsync), - _options.RetryOptions, - x))); - - // Create a new orchestration with the same instance ID to process the remaining data - logger.LogInformation("Completed cleaning up frame range data in the range {Range}. Continuing with new execution...", batchRange); - - WatermarkRange completed = input.Completed.HasValue - ? new WatermarkRange(batchRange.Start, input.Completed.Value.End) - : batchRange; - - context.ContinueAsNew( - new DataCleanupCheckPoint - { - Batching = input.Batching, - Completed = completed, - CreatedTime = input.CreatedTime ?? await context.GetCreatedTimeAsync(_options.RetryOptions), - StartFilterTimeStamp = input.StartFilterTimeStamp, - EndFilterTimeStamp = input.EndFilterTimeStamp - }); - } - else - { - logger.LogInformation("Completed cleaning up frame range data operation."); - } - } -} diff --git a/src/Microsoft.Health.Dicom.Functions/DataCleanup/DataCleanupDurableFunction.cs b/src/Microsoft.Health.Dicom.Functions/DataCleanup/DataCleanupDurableFunction.cs deleted file mode 100644 index 44b0f14cd9..0000000000 --- a/src/Microsoft.Health.Dicom.Functions/DataCleanup/DataCleanupDurableFunction.cs +++ /dev/null @@ -1,32 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Retrieve; -using Microsoft.Health.Dicom.Core.Features.Store; - -namespace Microsoft.Health.Dicom.Functions.DataCleanup; - -public partial class DataCleanupDurableFunction -{ - private readonly IInstanceStore _instanceStore; - private readonly IIndexDataStore _indexDataStore; - private readonly IMetadataStore _metadataStore; - private readonly DataCleanupOptions _options; - - public DataCleanupDurableFunction( - IInstanceStore instanceStore, - IIndexDataStore indexDataStore, - IMetadataStore metadataStore, - IOptions configOptions) - { - _instanceStore = EnsureArg.IsNotNull(instanceStore, nameof(instanceStore)); - _indexDataStore = EnsureArg.IsNotNull(indexDataStore, nameof(indexDataStore)); - _metadataStore = EnsureArg.IsNotNull(metadataStore, nameof(metadataStore)); - _options = EnsureArg.IsNotNull(configOptions?.Value, nameof(configOptions)); - } -} diff --git a/src/Microsoft.Health.Dicom.Functions/DataCleanup/DataCleanupOptions.cs b/src/Microsoft.Health.Dicom.Functions/DataCleanup/DataCleanupOptions.cs deleted file mode 100644 index fc265bb2c4..0000000000 --- a/src/Microsoft.Health.Dicom.Functions/DataCleanup/DataCleanupOptions.cs +++ /dev/null @@ -1,26 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.ComponentModel.DataAnnotations; -using Microsoft.Health.Operations.Functions.DurableTask; - -namespace Microsoft.Health.Dicom.Functions.DataCleanup; - -public class DataCleanupOptions -{ - internal const string SectionName = "DataCleanup"; - - /// - /// Gets or sets the number of threads available for each batch. - /// - [Range(-1, int.MaxValue)] - public int MaxParallelThreads { get; set; } = -1; - - /// - /// Gets or sets the for cleanup activities. - /// - public ActivityRetryOptions RetryOptions { get; set; } -} diff --git a/src/Microsoft.Health.Dicom.Functions/DataCleanup/Models/DataCleanupBatchCreationArguments.cs b/src/Microsoft.Health.Dicom.Functions/DataCleanup/Models/DataCleanupBatchCreationArguments.cs deleted file mode 100644 index 9c4a04723b..0000000000 --- a/src/Microsoft.Health.Dicom.Functions/DataCleanup/Models/DataCleanupBatchCreationArguments.cs +++ /dev/null @@ -1,63 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using EnsureThat; - -namespace Microsoft.Health.Dicom.Functions.DataCleanup.Models; - -public class DataCleanupBatchCreationArguments -{ - /// - /// Gets or sets the optional inclusive maximum watermark. - /// - public long? MaxWatermark { get; } - - /// - /// Gets or sets the number of DICOM instances processed by a single activity. - /// - public int BatchSize { get; } - - /// - /// Gets or sets the maximum number of concurrent batches processed at a given time. - /// - public int MaxParallelBatches { get; } - - /// - /// Gets or sets the start filter stamp - /// - public DateTimeOffset StartFilterTimeStamp { get; } - - /// - /// Gets or sets the end filter stamp - /// - public DateTimeOffset EndFilterTimeStamp { get; } - - /// - /// Initializes a new instance of the class with the specified values. - /// - /// The optional inclusive maximum watermark. - /// The number of DICOM instances processed by a single activity. - /// The maximum number of concurrent batches processed at a given time. - /// Start filter stamp - /// End filter stamp - /// - /// is less than 1. - /// -or- - /// is less than 1. - /// - public DataCleanupBatchCreationArguments(long? maxWatermark, int batchSize, int maxParallelBatches, DateTimeOffset startFilterTimeStamp, DateTimeOffset endFilterTimeStamp) - { - EnsureArg.IsGte(batchSize, 1, nameof(batchSize)); - EnsureArg.IsGte(maxParallelBatches, 1, nameof(maxParallelBatches)); - EnsureArg.IsTrue(startFilterTimeStamp <= endFilterTimeStamp, nameof(startFilterTimeStamp)); - - BatchSize = batchSize; - MaxParallelBatches = maxParallelBatches; - MaxWatermark = maxWatermark; - StartFilterTimeStamp = startFilterTimeStamp; - EndFilterTimeStamp = endFilterTimeStamp; - } -} diff --git a/src/Microsoft.Health.Dicom.Functions/Export/ExportDurableFunction.Activity.cs b/src/Microsoft.Health.Dicom.Functions/Export/ExportDurableFunction.Activity.cs deleted file mode 100644 index 3562fb299e..0000000000 --- a/src/Microsoft.Health.Dicom.Functions/Export/ExportDurableFunction.Activity.cs +++ /dev/null @@ -1,87 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Microsoft.Extensions.Logging; -using Microsoft.Health.Dicom.Core.Features.Export; -using Microsoft.Health.Dicom.Core.Models.Export; -using Microsoft.Health.Dicom.Functions.Export.Models; -using Microsoft.Health.Operations.Functions.DurableTask; - -namespace Microsoft.Health.Dicom.Functions.Export; - -public partial class ExportDurableFunction -{ - /// - /// Asynchronously exports a batch of DICOM files to a user-specified sink. - /// - /// The context for the activity. - /// A diagnostic logger. - /// - /// A task representing the operation. - /// The value of its property contains the number a summary of the export - /// operation's progress. - /// - /// - /// or is . - /// - [FunctionName(nameof(ExportBatchAsync))] - public async Task ExportBatchAsync([ActivityTrigger] IDurableActivityContext context, ILogger logger) - { - EnsureArg.IsNotNull(context, nameof(context)); - EnsureArg.IsNotNull(logger, nameof(logger)); - - ExportBatchArguments args = context.GetInput(); - await using IExportSource source = await _sourceFactory.CreateAsync(args.Source, args.Partition); - await using IExportSink sink = await _sinkFactory.CreateAsync(args.Destination, context.GetOperationId()); - - source.ReadFailure += (source, e) => logger.LogError(e.Exception, "Cannot read desired DICOM file(s)"); - sink.CopyFailure += (source, e) => logger.LogError(e.Exception, "Unable to copy watermark {Watermark}", e.Identifier.Version); - - // Copy files - int successes = 0, failures = 0; - await Parallel.ForEachAsync( - source, - new ParallelOptions - { - CancellationToken = default, - MaxDegreeOfParallelism = _options.MaxParallelThreads, - }, - async (result, token) => - { - if (await sink.CopyAsync(result, token)) - Interlocked.Increment(ref successes); - else - Interlocked.Increment(ref failures); - }); - - // Flush any files or errors - await sink.FlushAsync(); - - logger.LogInformation("Successfully exported {Files} DCM files.", successes); - if (failures > 0) - logger.LogWarning("Failed to export {Files} DCM files.", failures); - - return new ExportProgress(successes, failures); - } - - /// - /// Asynchronously completes a copy operation to the sink. - /// - /// The options for a specific sink type. - /// A task representing the operation. - /// is . - [FunctionName(nameof(CompleteCopyAsync))] - public Task CompleteCopyAsync([ActivityTrigger] ExportDataOptions destination) - { - EnsureArg.IsNotNull(destination, nameof(destination)); - return _sinkFactory.CompleteCopyAsync(destination); - } -} diff --git a/src/Microsoft.Health.Dicom.Functions/Export/ExportDurableFunction.Orchestration.cs b/src/Microsoft.Health.Dicom.Functions/Export/ExportDurableFunction.Orchestration.cs deleted file mode 100644 index f1bb196cb1..0000000000 --- a/src/Microsoft.Health.Dicom.Functions/Export/ExportDurableFunction.Orchestration.cs +++ /dev/null @@ -1,94 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Microsoft.Extensions.Logging; -using Microsoft.Health.Dicom.Core.Features.Export; -using Microsoft.Health.Dicom.Core.Models.Export; -using Microsoft.Health.Dicom.Functions.Export.Models; -using Microsoft.Health.Operations.Functions.DurableTask; - -namespace Microsoft.Health.Dicom.Functions.Export; - -public partial class ExportDurableFunction -{ - /// - /// Asynchronously exports DICOM files to a user-specified sink. - /// - /// The context for the orchestration instance. - /// A diagnostic logger. - /// A task representing the operation. - /// - /// or is . - /// - /// Orchestration instance ID is invalid. - [FunctionName(nameof(ExportDicomFilesAsync))] - public async Task ExportDicomFilesAsync([OrchestrationTrigger] IDurableOrchestrationContext context, ILogger logger) - { - EnsureArg.IsNotNull(context, nameof(context)).ThrowIfInvalidOperationId(); - logger = context.CreateReplaySafeLogger(EnsureArg.IsNotNull(logger, nameof(logger))); - - ExportCheckpoint input = context.GetInput(); - - // Are we done? - if (input.Source == null) - { - await context.CallActivityWithRetryAsync(nameof(CompleteCopyAsync), _options.RetryOptions, input.Destination); - - logger.LogInformation("Completed export to '{Sink}'.", input.Destination.Type); - return; - } - - // Get batches - logger.LogInformation( - "Starting to export to '{Sink}'. Exported {Exported} files so far. Skipped {Skipped} resources.", - input.Destination.Type, - input.Progress.Exported, - input.Progress.Skipped); - - await using IExportSource source = await _sourceFactory.CreateAsync(input.Source, input.Partition); - - // Start export in parallel - var exportTasks = new List>(); - for (int i = 0; i < input.Batching.MaxParallelCount; i++) - { - if (!source.TryDequeueBatch(input.Batching.Size, out ExportDataOptions batch)) - break; // All done - - exportTasks.Add(context.CallActivityWithRetryAsync( - nameof(ExportBatchAsync), - _options.RetryOptions, - new ExportBatchArguments - { - Destination = input.Destination, - Partition = input.Partition, - Source = batch, - })); - } - - // Await the export and count how many instances were exported - ExportProgress[] exportResults = await Task.WhenAll(exportTasks); - ExportProgress iterationProgress = exportResults.Aggregate(default, (x, y) => x + y); - - // Export the next set of batches - context.ContinueAsNew( - new ExportCheckpoint - { - Batching = input.Batching, - CreatedTime = input.CreatedTime ?? await context.GetCreatedTimeAsync(_options.RetryOptions), - Destination = input.Destination, - ErrorHref = input.ErrorHref, - Progress = input.Progress + iterationProgress, - Source = source.Description, - Partition = input.Partition - }); - } -} diff --git a/src/Microsoft.Health.Dicom.Functions/Export/ExportDurableFunction.cs b/src/Microsoft.Health.Dicom.Functions/Export/ExportDurableFunction.cs deleted file mode 100644 index 43187f656f..0000000000 --- a/src/Microsoft.Health.Dicom.Functions/Export/ExportDurableFunction.cs +++ /dev/null @@ -1,37 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using EnsureThat; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Features.Export; - -namespace Microsoft.Health.Dicom.Functions.Export; - -/// -/// Represents the Azure Durable Function that perform the export of data from Azure Health Data Services to a pre-defined sink. -/// -public partial class ExportDurableFunction -{ - private readonly ExportSourceFactory _sourceFactory; - private readonly ExportSinkFactory _sinkFactory; - private readonly ExportOptions _options; - - /// - /// Initializes a new instance of the class. - /// - /// A factory for creating instances. - /// A factory for creating instances. - /// A collection of settings related to the execution of export operations. - /// - /// , , or is . - /// - public ExportDurableFunction(ExportSourceFactory sourceFactory, ExportSinkFactory sinkFactory, IOptions options) - { - _sourceFactory = EnsureArg.IsNotNull(sourceFactory, nameof(sinkFactory)); - _sinkFactory = EnsureArg.IsNotNull(sinkFactory, nameof(sinkFactory)); - _options = EnsureArg.IsNotNull(options?.Value, nameof(options)); - } -} diff --git a/src/Microsoft.Health.Dicom.Functions/Export/ExportOptions.cs b/src/Microsoft.Health.Dicom.Functions/Export/ExportOptions.cs deleted file mode 100644 index 86fb3fd863..0000000000 --- a/src/Microsoft.Health.Dicom.Functions/Export/ExportOptions.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.ComponentModel.DataAnnotations; -using Microsoft.Health.Operations.Functions.DurableTask; - -namespace Microsoft.Health.Dicom.Functions.Export; - -/// -/// Represents configurable settings that control the execution of export operations. -/// -public class ExportOptions -{ - internal const string SectionName = "Export"; - - /// - /// Gets or sets the number of threads available for each batch. - /// - [Range(-1, int.MaxValue)] - public int MaxParallelThreads { get; set; } = -1; - - /// - /// Gets or sets the for export activities. - /// - [Required] - public ActivityRetryOptions RetryOptions { get; set; } -} diff --git a/src/Microsoft.Health.Dicom.Functions/Export/Models/ExportBatchArguments.cs b/src/Microsoft.Health.Dicom.Functions/Export/Models/ExportBatchArguments.cs deleted file mode 100644 index 00171798f1..0000000000 --- a/src/Microsoft.Health.Dicom.Functions/Export/Models/ExportBatchArguments.cs +++ /dev/null @@ -1,33 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Core.Models.Export; - -namespace Microsoft.Health.Dicom.Functions.Export.Models; - -/// -/// Represents the arguments to the activity. -/// -public class ExportBatchArguments -{ - /// - /// Gets or sets the source batch for the export operation. - /// - /// The configuration describing the batch of data from the source. - public ExportDataOptions Source { get; set; } - - /// - /// Gets or sets the destination of the export operation. - /// - /// The configuration describing the destination. - public ExportDataOptions Destination { get; set; } - - /// - /// Gets or sets the DICOM data partition from which the data is read. - /// - /// A DICOM partition entry. - public Partition Partition { get; set; } -} diff --git a/src/Microsoft.Health.Dicom.Functions/Indexing/Models/BatchCreationArguments.cs b/src/Microsoft.Health.Dicom.Functions/Indexing/Models/BatchCreationArguments.cs deleted file mode 100644 index 5669cda509..0000000000 --- a/src/Microsoft.Health.Dicom.Functions/Indexing/Models/BatchCreationArguments.cs +++ /dev/null @@ -1,51 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using EnsureThat; - -namespace Microsoft.Health.Dicom.Functions.Indexing.Models; - -/// -/// Represents the options for creating batches for re-indexing. -/// -public sealed class BatchCreationArguments -{ - /// - /// Gets or sets the optional inclusive maximum watermark. - /// - public long? MaxWatermark { get; } - - /// - /// Gets or sets the number of DICOM instances processed by a single activity. - /// - public int BatchSize { get; } - - /// - /// Gets or sets the maximum number of concurrent batches processed at a given time. - /// - public int MaxParallelBatches { get; } - - /// - /// Initializes a new instance of the class with the specified values. - /// - /// The optional inclusive maximum watermark. - /// The number of DICOM instances processed by a single activity. - /// The maximum number of concurrent batches processed at a given time. - /// - /// is less than 1. - /// -or- - /// is less than 1. - /// - public BatchCreationArguments(long? maxWatermark, int batchSize, int maxParallelBatches) - { - EnsureArg.IsGte(batchSize, 1, nameof(batchSize)); - EnsureArg.IsGte(maxParallelBatches, 1, nameof(maxParallelBatches)); - - BatchSize = batchSize; - MaxParallelBatches = maxParallelBatches; - MaxWatermark = maxWatermark; - } -} diff --git a/src/Microsoft.Health.Dicom.Functions/Indexing/Models/ReindexBatch.cs b/src/Microsoft.Health.Dicom.Functions/Indexing/Models/ReindexBatch.cs deleted file mode 100644 index 4d978c16d2..0000000000 --- a/src/Microsoft.Health.Dicom.Functions/Indexing/Models/ReindexBatch.cs +++ /dev/null @@ -1,31 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Features.Model; - -namespace Microsoft.Health.Dicom.Functions.Indexing.Models; - -/// -/// Represents input to -/// -[Obsolete("Please use ReindexBatchArguments instead.")] -public class ReindexBatch -{ - /// - /// Gets or sets the inclusive watermark range. - /// - public WatermarkRange WatermarkRange { get; set; } - - /// - /// Gets or sets the tag entries. - /// - public IReadOnlyCollection QueryTags { get; set; } - - internal ReindexBatchArguments ToArguments() - => new ReindexBatchArguments(QueryTags, WatermarkRange); -} diff --git a/src/Microsoft.Health.Dicom.Functions/Indexing/Models/ReindexBatchArguments.cs b/src/Microsoft.Health.Dicom.Functions/Indexing/Models/ReindexBatchArguments.cs deleted file mode 100644 index 5187c58cc8..0000000000 --- a/src/Microsoft.Health.Dicom.Functions/Indexing/Models/ReindexBatchArguments.cs +++ /dev/null @@ -1,44 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using EnsureThat; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Features.Model; - -namespace Microsoft.Health.Dicom.Functions.Indexing.Models; - -/// -/// Represents input to -/// -public sealed class ReindexBatchArguments -{ - /// - /// Gets or sets the tag entries. - /// - public IReadOnlyCollection QueryTags { get; } - - /// - /// Gets or sets the inclusive watermark range. - /// - public WatermarkRange WatermarkRange { get; } - - /// - /// Initializes a new instance of the class with the specified values. - /// - /// The tag entries. - /// The inclusive watermark range. - /// is . - public ReindexBatchArguments( - IReadOnlyCollection queryTags, - WatermarkRange watermarkRange) - { - EnsureArg.IsNotNull(queryTags, nameof(queryTags)); - - QueryTags = queryTags; - WatermarkRange = watermarkRange; - } -} diff --git a/src/Microsoft.Health.Dicom.Functions/Indexing/QueryTagIndexingOptions.cs b/src/Microsoft.Health.Dicom.Functions/Indexing/QueryTagIndexingOptions.cs deleted file mode 100644 index 15a56bc6c3..0000000000 --- a/src/Microsoft.Health.Dicom.Functions/Indexing/QueryTagIndexingOptions.cs +++ /dev/null @@ -1,41 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.ComponentModel.DataAnnotations; -using Microsoft.Health.Operations.Functions.DurableTask; - -namespace Microsoft.Health.Dicom.Functions.Indexing; - -/// -/// Represents the options for a "re-index" function. -/// -public class QueryTagIndexingOptions -{ - internal const string SectionName = "Indexing"; - - /// - /// Gets or sets the number of DICOM instances processed by a single activity. - /// - [Range(1, int.MaxValue)] - public int BatchSize { get; set; } = 100; - - /// - /// Gets or sets the number of threads available for each batch. - /// - [Range(-1, int.MaxValue)] - public int MaxParallelThreads { get; set; } = -1; - - /// - /// Gets or sets the maximum number of concurrent batches processed at a given time. - /// - [Range(1, int.MaxValue)] - public int MaxParallelBatches { get; set; } = 10; - - /// - /// Gets or sets the for re-indexing activities. - /// - public ActivityRetryOptions RetryOptions { get; set; } -} diff --git a/src/Microsoft.Health.Dicom.Functions/Indexing/ReindexDurableFunction.Activity.cs b/src/Microsoft.Health.Dicom.Functions/Indexing/ReindexDurableFunction.Activity.cs deleted file mode 100644 index f79cb02736..0000000000 --- a/src/Microsoft.Health.Dicom.Functions/Indexing/ReindexDurableFunction.Activity.cs +++ /dev/null @@ -1,264 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Microsoft.Extensions.Logging; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Models; -using Microsoft.Health.Dicom.Functions.Indexing.Models; -using Microsoft.Health.Operations.Functions.DurableTask; - -namespace Microsoft.Health.Dicom.Functions.Indexing; - -public partial class ReindexDurableFunction -{ - /// - /// Asynchronously assigns the to the given tag keys. - /// - /// - /// If the tags were not previously associated with the operation ID, this operation will create the association. - /// - /// The context for the activity. - /// A diagnostic logger. - /// - /// A task representing the operation. - /// The value of its property contains the subset of query tags - /// that have been associated the operation. - /// - /// - /// or is . - /// - [FunctionName(nameof(AssignReindexingOperationAsync))] - public Task> AssignReindexingOperationAsync( - [ActivityTrigger] IDurableActivityContext context, - ILogger logger) - { - EnsureArg.IsNotNull(context, nameof(context)); - EnsureArg.IsNotNull(logger, nameof(logger)); - - IReadOnlyList tagKeys = context.GetInput>(); - logger.LogInformation("Assigning {Count} query tags to operation ID '{OperationId}': {{{TagKeys}}}", - tagKeys.Count, - context.InstanceId, - string.Join(", ", tagKeys)); - - return _extendedQueryTagStore.AssignReindexingOperationAsync( - tagKeys, - context.GetOperationId(), - returnIfCompleted: false, - cancellationToken: CancellationToken.None); - } - - /// - /// Asynchronously retrieves the query tags that have been associated with the operation. - /// - /// The context for the activity. - /// A diagnostic logger. - /// - /// A task representing the operation. - /// The value of its property contains the subset of query tags - /// that have been associated the operation. - /// - /// - /// or is . - /// - [FunctionName(nameof(GetQueryTagsAsync))] - public Task> GetQueryTagsAsync( - [ActivityTrigger] IDurableActivityContext context, - ILogger logger) - { - EnsureArg.IsNotNull(context, nameof(context)); - EnsureArg.IsNotNull(logger, nameof(logger)); - - logger.LogInformation( - "Fetching the extended query tags for operation ID '{OperationId}'.", - context.InstanceId); - - return _extendedQueryTagStore.GetExtendedQueryTagsAsync( - context.GetOperationId(), - cancellationToken: CancellationToken.None); - } - - /// - /// Asynchronously retrieves the next set of instance batches based on the configured options. - /// - /// The optional inclusive maximum watermark. - /// A diagnostic logger. - /// - /// A task representing the asynchronous get operation. The value of its - /// property contains a list of batches as defined by their smallest and largest watermark. - /// - /// is . - [Obsolete("Please use GetInstanceBatchesV2Async instead.")] - [FunctionName(nameof(GetInstanceBatchesAsync))] - public Task> GetInstanceBatchesAsync([ActivityTrigger] long? maxWatermark, ILogger logger) - => GetInstanceBatchesV2Async(new BatchCreationArguments(maxWatermark, _options.BatchSize, _options.MaxParallelBatches), logger); - - /// - /// Asynchronously retrieves the next set of instance batches based on the configured options. - /// - /// The options for configuring the batches. - /// A diagnostic logger. - /// - /// A task representing the asynchronous get operation. The value of its - /// property contains a list of batches as defined by their smallest and largest watermark. - /// - /// - /// or is . - /// - [FunctionName(nameof(GetInstanceBatchesV2Async))] - public Task> GetInstanceBatchesV2Async( - [ActivityTrigger] BatchCreationArguments arguments, - ILogger logger) - { - EnsureArg.IsNotNull(arguments, nameof(arguments)); - EnsureArg.IsNotNull(logger, nameof(logger)); - - if (arguments.MaxWatermark.HasValue) - { - logger.LogInformation("Dividing up the instances into batches starting from the largest watermark {Watermark}.", arguments.MaxWatermark); - } - else - { - logger.LogInformation("Dividing up the instances into batches starting from the end."); - } - - return _instanceStore.GetInstanceBatchesAsync( - arguments.BatchSize, - arguments.MaxParallelBatches, - IndexStatus.Created, - arguments.MaxWatermark, - CancellationToken.None); - } - - /// - /// Asynchronously re-indexes a range of data. - /// - /// The batch that should be re-indexed including the range of data and the new tags. - /// A diagnostic logger. - /// A task representing the operation. - /// - /// or is . - /// - [Obsolete("Please use ReindexBatchV2Async instead.")] - [FunctionName(nameof(ReindexBatchAsync))] - public Task ReindexBatchAsync([ActivityTrigger] ReindexBatch batch, ILogger logger) - => ReindexBatchV2Async(batch?.ToArguments(), logger); - - /// - /// Asynchronously re-indexes a range of data. - /// - /// The options that include the instances to re-index and the query tags. - /// A diagnostic logger. - /// A task representing the operation. - /// - /// or is . - /// - [FunctionName(nameof(ReindexBatchV2Async))] - public async Task ReindexBatchV2Async([ActivityTrigger] ReindexBatchArguments arguments, ILogger logger) - { - EnsureArg.IsNotNull(arguments, nameof(arguments)); - EnsureArg.IsNotNull(logger, nameof(logger)); - - string tags = string.Join(", ", arguments.QueryTags.Select(x => x.Path)); - logger.LogInformation("Beginning to re-index instances in the range {Range} for the {TagCount} query tags {{{Tags}}}", - arguments.WatermarkRange, - arguments.QueryTags.Count, - tags); - - IReadOnlyList instanceIdentifiers = - await _instanceStore.GetInstanceIdentifiersByWatermarkRangeAsync(arguments.WatermarkRange, IndexStatus.Created); - - var missing = new ConcurrentDictionary(); - - await Parallel.ForEachAsync( - instanceIdentifiers, - new ParallelOptions - { - CancellationToken = default, - MaxDegreeOfParallelism = _options.MaxParallelThreads, - }, - async (id, token) => - { - // Record any missing instances when re-indexing - try - { - await _instanceReindexer.ReindexInstanceAsync(arguments.QueryTags, id, token); - } - catch (ItemNotFoundException ex) - { - missing[id] = ex; - } - }); - - // If there are any missing, re-query the batch and ensure they were deleted - if (!missing.IsEmpty) - { - logger.LogWarning( - "Could not find {Count} instances to re-index including watermark {Watermark}", - missing.Count, - missing.Keys.First().Version); - - instanceIdentifiers = await _instanceStore.GetInstanceIdentifiersByWatermarkRangeAsync(arguments.WatermarkRange, IndexStatus.Created); - foreach (VersionedInstanceIdentifier identifier in instanceIdentifiers) - { - // If the identifier is still present, then throw! - if (missing.TryGetValue(identifier, out ItemNotFoundException exception)) - { - logger.LogError( - "Metadata is missing for {Count} instances including watermark {Watermark}", - instanceIdentifiers.Count(x => missing.ContainsKey(x)), - identifier.Version); - throw exception; - } - } - } - - logger.LogInformation("Completed re-indexing instances in the range {Range} for the {TagCount} query tags {{{Tags}}}", - arguments.WatermarkRange, - arguments.QueryTags.Count, - tags); - } - - /// - /// Asynchronously completes the operation by removing the association between the tags and the operation. - /// - /// The context for the activity. - /// A diagnostic logger. - /// - /// A task representing the operation. - /// The value of its property contains the set of extended query tags - /// whose re-indexing should be considered completed. - /// - /// - /// or is . - /// - [FunctionName(nameof(CompleteReindexingAsync))] - public Task> CompleteReindexingAsync( - [ActivityTrigger] IDurableActivityContext context, - ILogger logger) - { - EnsureArg.IsNotNull(context, nameof(context)); - EnsureArg.IsNotNull(logger, nameof(logger)); - - IReadOnlyList tagKeys = context.GetInput>(); - logger.LogInformation("Completing the re-indexing operation {OperationId} for {Count} query tags {{{TagKeys}}}", - context.InstanceId, - tagKeys.Count, - string.Join(", ", tagKeys)); - - return _extendedQueryTagStore.CompleteReindexingAsync(tagKeys, CancellationToken.None); - } -} diff --git a/src/Microsoft.Health.Dicom.Functions/Indexing/ReindexDurableFunction.Orchestration.cs b/src/Microsoft.Health.Dicom.Functions/Indexing/ReindexDurableFunction.Orchestration.cs deleted file mode 100644 index e77dfe43e2..0000000000 --- a/src/Microsoft.Health.Dicom.Functions/Indexing/ReindexDurableFunction.Orchestration.cs +++ /dev/null @@ -1,130 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Microsoft.Extensions.Logging; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Functions.Indexing.Models; -using Microsoft.Health.Operations.Functions.DurableTask; - -namespace Microsoft.Health.Dicom.Functions.Indexing; - -public partial class ReindexDurableFunction -{ - /// - /// Asynchronously creates an index for the provided query tags over the previously added data. - /// - /// - /// Durable functions are reliable, and their implementations will be executed repeatedly over the lifetime of - /// a single instance. - /// - /// The context for the orchestration instance. - /// A diagnostic logger. - /// A task representing the operation. - /// - /// or is . - /// - /// Orchestration instance ID is invalid. - [FunctionName(nameof(ReindexInstancesAsync))] - public async Task ReindexInstancesAsync( - [OrchestrationTrigger] IDurableOrchestrationContext context, - ILogger logger) - { - EnsureArg.IsNotNull(context, nameof(context)).ThrowIfInvalidOperationId(); - logger = context.CreateReplaySafeLogger(EnsureArg.IsNotNull(logger, nameof(logger))); - - ReindexCheckpoint input = context.GetInput(); - - // Backfill batching options - input.Batching ??= new BatchingOptions - { - MaxParallelCount = _options.MaxParallelBatches, - Size = _options.BatchSize, - }; - - // Fetch the set of query tags that require re-indexing - IReadOnlyList queryTags = await GetOperationQueryTagsAsync(context, input); - logger.LogInformation( - "Found {Count} extended query tag paths to re-index {{{TagPaths}}}.", - queryTags.Count, - string.Join(", ", queryTags.Select(x => x.Path))); - - List queryTagKeys = queryTags.Select(x => x.Key).ToList(); - if (queryTags.Count > 0) - { - IReadOnlyList batches = await context.CallActivityWithRetryAsync>( - nameof(GetInstanceBatchesV2Async), - _options.RetryOptions, - new BatchCreationArguments(input.Completed?.Start - 1, input.Batching.Size, input.Batching.MaxParallelCount)); - - if (batches.Count > 0) - { - // Note that batches are in reverse order because we start from the highest watermark - var batchRange = new WatermarkRange(batches[^1].Start, batches[0].End); - - logger.LogInformation("Beginning to re-index the range {Range}.", batchRange); - await Task.WhenAll(batches - .Select(x => context.CallActivityWithRetryAsync( - nameof(ReindexBatchV2Async), - _options.RetryOptions, - new ReindexBatchArguments(queryTags, x)))); - - // Create a new orchestration with the same instance ID to process the remaining data - logger.LogInformation("Completed re-indexing the range {Range}. Continuing with new execution...", batchRange); - - WatermarkRange completed = input.Completed.HasValue - ? new WatermarkRange(batchRange.Start, input.Completed.Value.End) - : batchRange; - - context.ContinueAsNew( - new ReindexCheckpoint - { - Batching = input.Batching, - Completed = completed, - CreatedTime = input.CreatedTime ?? await context.GetCreatedTimeAsync(_options.RetryOptions), - QueryTagKeys = queryTagKeys, - }); - } - else - { - IReadOnlyList completed = await context.CallActivityWithRetryAsync>( - nameof(CompleteReindexingAsync), - _options.RetryOptions, - queryTagKeys); - - logger.LogInformation( - "Completed re-indexing for the following extended query tags {{{QueryTagKeys}}}.", - string.Join(", ", completed)); - } - } - else - { - logger.LogWarning( - "Could not find any query tags for the re-indexing operation '{OperationId}'.", - context.InstanceId); - } - } - - // Determine the set of query tags that should be indexed and only continue if there is at least 1. - // For the first time this orchestration executes, assign all of the tags in the input to the operation, - // otherwise simply fetch the tags from the database for this operation. - private Task> GetOperationQueryTagsAsync(IDurableOrchestrationContext context, ReindexCheckpoint input) - => input.Completed.HasValue - ? context.CallActivityWithRetryAsync>( - nameof(GetQueryTagsAsync), - _options.RetryOptions, - null) - : context.CallActivityWithRetryAsync>( - nameof(AssignReindexingOperationAsync), - _options.RetryOptions, - input.QueryTagKeys); -} diff --git a/src/Microsoft.Health.Dicom.Functions/Indexing/ReindexDurableFunction.cs b/src/Microsoft.Health.Dicom.Functions/Indexing/ReindexDurableFunction.cs deleted file mode 100644 index 2957f56dbf..0000000000 --- a/src/Microsoft.Health.Dicom.Functions/Indexing/ReindexDurableFunction.cs +++ /dev/null @@ -1,36 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Features.Indexing; -using Microsoft.Health.Dicom.Core.Features.Retrieve; - -namespace Microsoft.Health.Dicom.Functions.Indexing; - -/// -/// Represents the Azure Durable Functions that perform the re-indexing of previously added DICOM instances -/// based on new tags configured by the user. -/// -public partial class ReindexDurableFunction -{ - private readonly IExtendedQueryTagStore _extendedQueryTagStore; - private readonly IInstanceStore _instanceStore; - private readonly IInstanceReindexer _instanceReindexer; - private readonly QueryTagIndexingOptions _options; - - public ReindexDurableFunction( - IExtendedQueryTagStore extendedQueryTagStore, - IInstanceStore instanceStore, - IInstanceReindexer instanceReindexer, - IOptions configOptions) - { - _extendedQueryTagStore = EnsureArg.IsNotNull(extendedQueryTagStore, nameof(extendedQueryTagStore)); - _instanceStore = EnsureArg.IsNotNull(instanceStore, nameof(instanceStore)); - _instanceReindexer = EnsureArg.IsNotNull(instanceReindexer, nameof(instanceReindexer)); - _options = EnsureArg.IsNotNull(configOptions?.Value, nameof(configOptions)); - } -} diff --git a/src/Microsoft.Health.Dicom.Functions/MetricsCollection/IndexMetricsCollectionFunction.cs b/src/Microsoft.Health.Dicom.Functions/MetricsCollection/IndexMetricsCollectionFunction.cs deleted file mode 100644 index 888ff508b9..0000000000 --- a/src/Microsoft.Health.Dicom.Functions/MetricsCollection/IndexMetricsCollectionFunction.cs +++ /dev/null @@ -1,70 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.Azure.WebJobs; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.Store; -using Microsoft.Health.Functions.Extensions; - -namespace Microsoft.Health.Dicom.Functions.MetricsCollection; - -/// -/// A function for collecting index metrics -/// -public class IndexMetricsCollectionFunction -{ - private readonly IIndexDataStore _indexDataStore; - private readonly bool _externalStoreEnabled; - private readonly bool _enableDataPartitions; - private const string RunFrequencyVariable = $"%{AzureFunctionsJobHost.RootSectionName}:DicomFunctions:{IndexMetricsCollectionOptions.SectionName}:{nameof(IndexMetricsCollectionOptions.Frequency)}%"; - - public IndexMetricsCollectionFunction( - IIndexDataStore indexDataStore, - IOptions featureConfiguration) - { - EnsureArg.IsNotNull(featureConfiguration, nameof(featureConfiguration)); - _indexDataStore = EnsureArg.IsNotNull(indexDataStore, nameof(indexDataStore)); - _externalStoreEnabled = featureConfiguration.Value.EnableExternalStore; - _enableDataPartitions = featureConfiguration.Value.EnableDataPartitions; - } - - /// - /// Asynchronously collects index metrics. - /// - /// The timer which tracks the invocation schedule. - /// A diagnostic logger. - /// A task that represents the asynchronous metrics collection operation. - [FunctionName(nameof(IndexMetricsCollectionFunction))] - public async Task Run( - [TimerTrigger(RunFrequencyVariable)] TimerInfo invocationTimer, - ILogger log) - { - EnsureArg.IsNotNull(invocationTimer, nameof(invocationTimer)); - EnsureArg.IsNotNull(log, nameof(log)); - if (!_externalStoreEnabled) - { - log.LogInformation("External store is not enabled. Skipping index metrics collection."); - return; - } - - if (invocationTimer.IsPastDue) - { - log.LogWarning("Current function invocation is running late."); - } - IndexedFileProperties indexedFileProperties = await _indexDataStore.GetIndexedFileMetricsAsync(); - - log.LogInformation( - "DICOM telemetry - TotalFilesIndexed: {TotalFilesIndexed} , TotalByesIndexed: {TotalContentLengthIndexed} , with ExternalStoreEnabled: {ExternalStoreEnabled} and DataPartitionsEnabled: {PartitionsEnabled}", - indexedFileProperties.TotalIndexed, - indexedFileProperties.TotalSum, - _externalStoreEnabled, - _enableDataPartitions); - } -} \ No newline at end of file diff --git a/src/Microsoft.Health.Dicom.Functions/MetricsCollection/IndexMetricsCollectionOptions.cs b/src/Microsoft.Health.Dicom.Functions/MetricsCollection/IndexMetricsCollectionOptions.cs deleted file mode 100644 index 8eaeef4032..0000000000 --- a/src/Microsoft.Health.Dicom.Functions/MetricsCollection/IndexMetricsCollectionOptions.cs +++ /dev/null @@ -1,26 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.ComponentModel.DataAnnotations; - -namespace Microsoft.Health.Dicom.Functions.MetricsCollection; - -/// -/// Options on collecting indexing metrics -/// -public class IndexMetricsCollectionOptions -{ - /// - /// The default section name for in a configuration. - /// - public const string SectionName = "IndexMetricsCollection"; - - /// - /// Gets or sets the cron expression that indicates how frequently to run the index metrics collection function. - /// - /// A value cron expression - [Required] - public string Frequency { get; set; } -} diff --git a/src/Microsoft.Health.Dicom.Functions/Microsoft.Health.Dicom.Functions.csproj b/src/Microsoft.Health.Dicom.Functions/Microsoft.Health.Dicom.Functions.csproj deleted file mode 100644 index b7540ab1df..0000000000 --- a/src/Microsoft.Health.Dicom.Functions/Microsoft.Health.Dicom.Functions.csproj +++ /dev/null @@ -1,54 +0,0 @@ - - - - net6.0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ExportDurableFunction.cs - - - ReindexDurableFunction.cs - - - UpdateDurableFunction.cs - - - DataCleanupDurableFunction.cs - - - ContentLengthBackFillDurableFunction.cs - - - - diff --git a/src/Microsoft.Health.Dicom.Functions/Properties/AssemblyInfo.cs b/src/Microsoft.Health.Dicom.Functions/Properties/AssemblyInfo.cs deleted file mode 100644 index 73efa6f0f8..0000000000 --- a/src/Microsoft.Health.Dicom.Functions/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,12 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Resources; -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("Microsoft.Health.Dicom.Functions.UnitTests")] -[assembly: NeutralResourcesLanguage("en-us")] -[assembly: CLSCompliant(false)] diff --git a/src/Microsoft.Health.Dicom.Functions/Registration/DicomFunctionsBuilder.cs b/src/Microsoft.Health.Dicom.Functions/Registration/DicomFunctionsBuilder.cs deleted file mode 100644 index 3c4d721453..0000000000 --- a/src/Microsoft.Health.Dicom.Functions/Registration/DicomFunctionsBuilder.cs +++ /dev/null @@ -1,18 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Health.Dicom.Core.Registration; - -namespace Microsoft.Health.Dicom.Functions.Registration; - -internal class DicomFunctionsBuilder : IDicomFunctionsBuilder -{ - public DicomFunctionsBuilder(IServiceCollection services) - => Services = EnsureArg.IsNotNull(services, nameof(services)); - - public IServiceCollection Services { get; } -} diff --git a/src/Microsoft.Health.Dicom.Functions/Registration/DurableContextExtensions.cs b/src/Microsoft.Health.Dicom.Functions/Registration/DurableContextExtensions.cs deleted file mode 100644 index c4e15cee5e..0000000000 --- a/src/Microsoft.Health.Dicom.Functions/Registration/DurableContextExtensions.cs +++ /dev/null @@ -1,25 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Diagnostics.Metrics; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; - -namespace Microsoft.Health.Dicom.Functions.Registration; - -internal static class DurableContextExtensions -{ - /// - /// Returns an instance of Counter that is replay safe, ensuring the meter emits metric only when the orchestrator - /// is not replaying that line of code. - /// - /// The context object. - /// A metric counter. - /// An instance of a replay safe Counter. - public static ReplaySafeCounter CreateReplaySafeCounter(this IDurableOrchestrationContext context, Counter counter) where T : struct - { - return new ReplaySafeCounter(context, counter); - } -} - diff --git a/src/Microsoft.Health.Dicom.Functions/Registration/ReplaySafeCounter.cs b/src/Microsoft.Health.Dicom.Functions/Registration/ReplaySafeCounter.cs deleted file mode 100644 index 3068cbb430..0000000000 --- a/src/Microsoft.Health.Dicom.Functions/Registration/ReplaySafeCounter.cs +++ /dev/null @@ -1,34 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Diagnostics.Metrics; -using EnsureThat; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; - -namespace Microsoft.Health.Dicom.Functions.Registration; - -/// -/// Replay-safe counter that can emits the metric only when the orchestrator is not replaying. Other -/// methods in the Counter class can be added as required. -/// -public class ReplaySafeCounter where T : struct -{ - private readonly IDurableOrchestrationContext _context; - private readonly Counter _counter; - - internal ReplaySafeCounter(IDurableOrchestrationContext context, Counter counter) - { - _context = EnsureArg.IsNotNull(context, nameof(context)); - _counter = EnsureArg.IsNotNull(counter, nameof(counter)); - } - - public void Add(T count) - { - if (!_context.IsReplaying) - { - _counter.Add(count); - } - } -} diff --git a/src/Microsoft.Health.Dicom.Functions/Registration/ServiceCollectionExtensions.cs b/src/Microsoft.Health.Dicom.Functions/Registration/ServiceCollectionExtensions.cs deleted file mode 100644 index d3754fa944..0000000000 --- a/src/Microsoft.Health.Dicom.Functions/Registration/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,173 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Text.Json; -using EnsureThat; -using FellowOakDicom; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Microsoft.Azure.WebJobs.Host.Config; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Extensions; -using Microsoft.Health.Dicom.Core.Features.Audit; -using Microsoft.Health.Dicom.Core.Features.FellowOakDicom; -using Microsoft.Health.Dicom.Core.Features.Telemetry; -using Microsoft.Health.Dicom.Core.Modules; -using Microsoft.Health.Dicom.Core.Registration; -using Microsoft.Health.Dicom.Functions.Configuration; -using Microsoft.Health.Dicom.Functions.ContentLengthBackFill; -using Microsoft.Health.Dicom.Functions.DataCleanup; -using Microsoft.Health.Dicom.Functions.Export; -using Microsoft.Health.Dicom.Functions.Indexing; -using Microsoft.Health.Dicom.Functions.MetricsCollection; -using Microsoft.Health.Dicom.Functions.Update; -using Microsoft.Health.Dicom.SqlServer.Registration; -using Microsoft.Health.Extensions.DependencyInjection; -using Microsoft.Health.Operations.Functions.DurableTask; -using Microsoft.Health.Operations.Functions.Management; -using Microsoft.Health.SqlServer.Configs; -using Newtonsoft.Json; - -namespace Microsoft.Health.Dicom.Functions.Registration; - -/// -/// A collection of methods for configuring DICOM Azure Functions. -/// -public static class ServiceCollectionExtensions -{ - /// - /// Adds the core set of services required to run the DICOM operations as Azure Functions. - /// - /// The . - /// The root. - /// A corresponding to add additional services. - /// - /// or is . - /// - public static IDicomFunctionsBuilder ConfigureFunctions( - this IServiceCollection services, - IConfiguration configuration) - { - EnsureArg.IsNotNull(services, nameof(services)); - EnsureArg.IsNotNull(configuration, nameof(configuration)); - - services.RegisterModule(); - - return new DicomFunctionsBuilder(services - .AddRecyclableMemoryStreamManager(configuration) - .AddFellowOakDicomExtension() - .AddFunctionsOptions(configuration, DataCleanupOptions.SectionName) - .AddFunctionsOptions(configuration, ContentLengthBackFillOptions.SectionName) - .AddFunctionsOptions(configuration, ExportOptions.SectionName) - .AddFunctionsOptions(configuration, QueryTagIndexingOptions.SectionName, bindNonPublicProperties: true) - .AddFunctionsOptions(configuration, PurgeHistoryOptions.SectionName, isDicomFunction: false) - .AddFunctionsOptions(configuration, "DicomServer:Features", isDicomFunction: false) - .AddFunctionsOptions(configuration, UpdateOptions.SectionName) - .AddFunctionsOptions(configuration, IndexMetricsCollectionOptions.SectionName) - .ConfigureDurableFunctionSerialization() - .AddJsonSerializerOptions(o => o.ConfigureDefaultDicomSettings()) - .AddSingleton() - .AddSingleton()); - } - - /// - /// Adds the metadata store for the DICOM functions. - /// - /// The DICOM functions builder instance. - /// The host configuration for the functions. - /// The functions builder. - public static IDicomFunctionsBuilder AddBlobStorage(this IDicomFunctionsBuilder builder, IConfiguration configuration) - { - EnsureArg.IsNotNull(builder, nameof(builder)); - EnsureArg.IsNotNull(configuration, nameof(configuration)); - - return builder.AddBlobStorage(configuration, DicomFunctionsConfiguration.SectionName); - } - - /// - /// Adds MSSQL Server implementations for indexing DICOM data and storing its metadata. - /// - /// The . - /// The root. - /// The for additional methods calls. - /// - /// or is . - /// - public static IDicomFunctionsBuilder AddSqlServer(this IDicomFunctionsBuilder builder, IConfiguration configuration) - { - EnsureArg.IsNotNull(builder, nameof(builder)); - EnsureArg.IsNotNull(configuration, nameof(configuration)); - - return builder.AddSqlServer(c => configuration.GetSection(SqlServerDataStoreConfiguration.SectionName).Bind(c)); - } - - private static IServiceCollection AddFellowOakDicomExtension(this IServiceCollection services) - { - EnsureArg.IsNotNull(services, nameof(services)); - - // Note: Fellow Oak Services have already been added as part of the ServiceModule - services.TryAddEnumerable(ServiceDescriptor.Singleton()); - - CustomDicomImplementation.SetDicomImplementationClassUIDAndVersion(); - - return services; - } - - private static IServiceCollection AddFunctionsOptions( - this IServiceCollection services, - IConfiguration configuration, - string sectionName, - bool isDicomFunction = true, - bool bindNonPublicProperties = false) - where T : class - { - EnsureArg.IsNotNull(services, nameof(services)); - EnsureArg.IsNotNull(configuration, nameof(configuration)); - EnsureArg.IsNotEmptyOrWhiteSpace(sectionName, nameof(sectionName)); - - string path = isDicomFunction ? DicomFunctionsConfiguration.SectionName + ":" + sectionName : sectionName; - services - .AddOptions() - .Bind( - configuration.GetSection(path), - x => x.BindNonPublicProperties = bindNonPublicProperties) - .ValidateDataAnnotations(); - - return services; - } - - private static IServiceCollection AddJsonSerializerOptions(this IServiceCollection services, Action configure) - { - EnsureArg.IsNotNull(services, nameof(services)); - EnsureArg.IsNotNull(configure, nameof(configure)); - - // TODO: Configure System.Text.Json for Azure Functions MVC services when available - // and if we decide to expose HTTP services - //builder.AddJsonOptions(o => configure(o.JsonSerializerOptions)); - return services.Configure(configure); - } - - private static IServiceCollection ConfigureDurableFunctionSerialization(this IServiceCollection services) - { - EnsureArg.IsNotNull(services, nameof(services)); - - services.Configure(o => o.ConfigureDefaultDicomSettings()); - return services.Replace(ServiceDescriptor.Singleton()); - } - - private sealed class FellowOakExtensionConfiguration : IExtensionConfigProvider - { - private readonly IServiceProvider _serviceProvider; - - public FellowOakExtensionConfiguration(IServiceProvider serviceProvider) - => _serviceProvider = EnsureArg.IsNotNull(serviceProvider, nameof(serviceProvider)); - - public void Initialize(ExtensionConfigContext context) - => DicomSetupBuilder.UseServiceProvider(_serviceProvider); - } -} diff --git a/src/Microsoft.Health.Dicom.Functions/Update/Models/CleanupBlobArgumentsV2.cs b/src/Microsoft.Health.Dicom.Functions/Update/Models/CleanupBlobArgumentsV2.cs deleted file mode 100644 index 414c33192b..0000000000 --- a/src/Microsoft.Health.Dicom.Functions/Update/Models/CleanupBlobArgumentsV2.cs +++ /dev/null @@ -1,38 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using EnsureThat; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Features.Partitioning; - -namespace Microsoft.Health.Dicom.Functions.Update.Models; - -/// -/// Used to pass args to cleanup tasks -/// -public sealed class CleanupBlobArgumentsV2 -{ - /// - /// Instances that need to be cleaned up - /// - public IReadOnlyList Instances { get; } - - /// - /// Partition within which the instances that need to be cleaned up reside - /// - public Partition Partition { get; } - - /// - /// Create cleanup args - /// - /// Instances that need to be cleaned up - /// Partition within which the instances that need to be cleaned up reside - public CleanupBlobArgumentsV2(IReadOnlyList instances, Partition partition) - { - Instances = EnsureArg.IsNotNull(instances, nameof(instances)); - Partition = EnsureArg.IsNotNull(partition, nameof(partition)); - } -} \ No newline at end of file diff --git a/src/Microsoft.Health.Dicom.Functions/Update/Models/CompleteStudyArgumentsV2.cs b/src/Microsoft.Health.Dicom.Functions/Update/Models/CompleteStudyArgumentsV2.cs deleted file mode 100644 index 5701f5bf83..0000000000 --- a/src/Microsoft.Health.Dicom.Functions/Update/Models/CompleteStudyArgumentsV2.cs +++ /dev/null @@ -1,29 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using EnsureThat; -using Microsoft.Health.Dicom.Core.Features.Model; - -namespace Microsoft.Health.Dicom.Functions.Update.Models; - -public class CompleteStudyArgumentsV2 -{ - public int PartitionKey { get; } - - public IReadOnlyList InstanceMetadataList { get; } - - public string StudyInstanceUid { get; } - - public string ChangeDataset { get; set; } - - public CompleteStudyArgumentsV2(int partitionKey, string studyInstanceUid, string changeDataset, IReadOnlyList instanceMetadataList) - { - PartitionKey = partitionKey; - StudyInstanceUid = EnsureArg.IsNotEmptyOrWhiteSpace(studyInstanceUid, nameof(studyInstanceUid)); - ChangeDataset = EnsureArg.IsNotNull(changeDataset, nameof(changeDataset)); - InstanceMetadataList = EnsureArg.IsNotNull(instanceMetadataList, nameof(instanceMetadataList)); - } -} diff --git a/src/Microsoft.Health.Dicom.Functions/Update/Models/UpdateInstanceBlobArgumentsV2.cs b/src/Microsoft.Health.Dicom.Functions/Update/Models/UpdateInstanceBlobArgumentsV2.cs deleted file mode 100644 index 57c624171e..0000000000 --- a/src/Microsoft.Health.Dicom.Functions/Update/Models/UpdateInstanceBlobArgumentsV2.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using EnsureThat; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Features.Partitioning; - -namespace Microsoft.Health.Dicom.Functions.Update.Models; - -/// -/// Represents input to -/// -public class UpdateInstanceBlobArgumentsV2 -{ - public Partition Partition { get; set; } - - public IReadOnlyList InstanceMetadataList { get; } - - public string ChangeDataset { get; } - - public UpdateInstanceBlobArgumentsV2(Partition partition, IReadOnlyList instanceMetadataList, string changeDataset) - { - Partition = EnsureArg.IsNotNull(partition, nameof(partition)); - InstanceMetadataList = EnsureArg.IsNotNull(instanceMetadataList, nameof(instanceMetadataList)); - ChangeDataset = EnsureArg.IsNotNull(changeDataset, nameof(changeDataset)); - } -} diff --git a/src/Microsoft.Health.Dicom.Functions/Update/Models/UpdateInstanceResponse.cs b/src/Microsoft.Health.Dicom.Functions/Update/Models/UpdateInstanceResponse.cs deleted file mode 100644 index 03e2a64db5..0000000000 --- a/src/Microsoft.Health.Dicom.Functions/Update/Models/UpdateInstanceResponse.cs +++ /dev/null @@ -1,23 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using EnsureThat; -using Microsoft.Health.Dicom.Core.Features.Model; - -namespace Microsoft.Health.Dicom.Functions.Update.Models; - -public class UpdateInstanceResponse -{ - public IReadOnlyList InstanceMetadataList { get; } - - public IReadOnlyList Errors { get; } - - public UpdateInstanceResponse(IReadOnlyList instanceMetadataList, IReadOnlyList errors) - { - InstanceMetadataList = EnsureArg.IsNotNull(instanceMetadataList, nameof(instanceMetadataList)); - Errors = errors; - } -} diff --git a/src/Microsoft.Health.Dicom.Functions/Update/Models/UpdateInstanceWatermarkArgumentsV2.cs b/src/Microsoft.Health.Dicom.Functions/Update/Models/UpdateInstanceWatermarkArgumentsV2.cs deleted file mode 100644 index 76d9c58d39..0000000000 --- a/src/Microsoft.Health.Dicom.Functions/Update/Models/UpdateInstanceWatermarkArgumentsV2.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using EnsureThat; -using Microsoft.Health.Dicom.Core.Features.Partitioning; - -namespace Microsoft.Health.Dicom.Functions.Update.Models; - -public class UpdateInstanceWatermarkArgumentsV2 -{ - public Partition Partition { get; } - - public string StudyInstanceUid { get; } - - public UpdateInstanceWatermarkArgumentsV2(Partition partition, string studyInstanceUid) - { - Partition = EnsureArg.IsNotNull(partition, nameof(partition)); - StudyInstanceUid = EnsureArg.IsNotEmptyOrWhiteSpace(studyInstanceUid, nameof(studyInstanceUid)); - } -} diff --git a/src/Microsoft.Health.Dicom.Functions/Update/UpdateDurableFunction.Activity.cs b/src/Microsoft.Health.Dicom.Functions/Update/UpdateDurableFunction.Activity.cs deleted file mode 100644 index 9a093607ae..0000000000 --- a/src/Microsoft.Health.Dicom.Functions/Update/UpdateDurableFunction.Activity.cs +++ /dev/null @@ -1,294 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using FellowOakDicom; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Microsoft.Extensions.Logging; -using Microsoft.Health.Dicom.Core.Exceptions; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Core.Features.Update; -using Microsoft.Health.Dicom.Functions.Update.Models; - -namespace Microsoft.Health.Dicom.Functions.Update; - -public partial class UpdateDurableFunction -{ - - /// - /// Asynchronously update instance new watermark. - /// - /// BatchUpdateArguments - /// A diagnostic logger. - /// - /// The result of the task contains the updated instances. - /// - /// - /// or is . - /// - [FunctionName(nameof(UpdateInstanceWatermarkV2Async))] - public async Task> UpdateInstanceWatermarkV2Async([ActivityTrigger] UpdateInstanceWatermarkArgumentsV2 arguments, ILogger logger) - { - EnsureArg.IsNotNull(arguments, nameof(arguments)); - EnsureArg.IsNotNull(arguments.StudyInstanceUid, nameof(arguments.StudyInstanceUid)); - EnsureArg.IsNotNull(logger, nameof(logger)); - - logger.LogInformation("Beginning to update all instance watermarks"); - - IEnumerable instanceMetadata = await _indexStore.BeginUpdateInstancesAsync(arguments.Partition, arguments.StudyInstanceUid, CancellationToken.None); - - logger.LogInformation("Beginning to update all instance watermarks"); - - return instanceMetadata; - } - - /// - /// Asynchronously batches the instance watermarks and calls the update instance. - /// - /// BatchUpdateArguments - /// A diagnostic logger. - /// - /// The result of the task contains the updated instances with file properties representing newly created blobs and any error. - /// - /// - /// or is . - /// - [FunctionName(nameof(UpdateInstanceBlobsV3Async))] - public async Task UpdateInstanceBlobsV3Async( - [ActivityTrigger] UpdateInstanceBlobArgumentsV2 arguments, - ILogger logger) - { - EnsureArg.IsNotNull(arguments, nameof(arguments)); - EnsureArg.IsNotNull(arguments.ChangeDataset, nameof(arguments.ChangeDataset)); - EnsureArg.IsNotNull(arguments.InstanceMetadataList, nameof(arguments.InstanceMetadataList)); - EnsureArg.IsNotNull(arguments.Partition, nameof(arguments.Partition)); - EnsureArg.IsNotNull(logger, nameof(logger)); - - DicomDataset datasetToUpdate = GetDeserializedDataset(arguments.ChangeDataset); - - logger.LogInformation("Beginning to update all instance blobs, Total count {TotalCount}", arguments.InstanceMetadataList.Count); - - var updatedInstances = new ConcurrentBag(); - var errors = new ConcurrentBag(); - - await Parallel.ForEachAsync( - arguments.InstanceMetadataList, - new ParallelOptions - { - CancellationToken = default, - MaxDegreeOfParallelism = _options.MaxParallelThreads, - }, - async (instance, token) => - { - try - { - FileProperties fileProperties = await _updateInstanceService.UpdateInstanceBlobAsync(instance, datasetToUpdate, arguments.Partition, token); - updatedInstances.Add( - new InstanceMetadata( - instance.VersionedInstanceIdentifier, - new InstanceProperties - { - FileProperties = fileProperties, - NewVersion = instance.InstanceProperties.NewVersion, - OriginalVersion = instance.InstanceProperties.OriginalVersion - })); - } - catch (DataStoreRequestFailedException ex) - { - logger.LogInformation("Failed to update instance with watermark {Watermark}, IsExternal {IsExternal}", instance.VersionedInstanceIdentifier.Version, ex.IsExternal); - errors.Add($"{ex.Message}. {ToInstanceString(instance.VersionedInstanceIdentifier)}"); - } - catch (DataStoreException ex) - { - logger.LogInformation("Failed to update instance with watermark {Watermark}, IsExternal {IsExternal}", instance.VersionedInstanceIdentifier.Version, ex.IsExternal); - errors.Add($"Failed to update instance. {ToInstanceString(instance.VersionedInstanceIdentifier)}"); - } - }); - - logger.LogInformation("Completed updating all instance blobs. Total instace count {TotalCount}. Total Failed {FailedCount}", arguments.InstanceMetadataList.Count, errors.Count); - - return new UpdateInstanceResponse(updatedInstances.ToList(), errors.ToList()); - } - - /// - /// Asynchronously commits all the instances in a study and creates new entries for changefeed. - /// - /// CompleteInstanceArguments - /// A diagnostic logger. - /// - /// A task representing the operation. - /// - /// - /// or is . - /// - [FunctionName(nameof(CompleteUpdateStudyV4Async))] - public async Task CompleteUpdateStudyV4Async([ActivityTrigger] CompleteStudyArgumentsV2 arguments, ILogger logger) - { - EnsureArg.IsNotNull(arguments, nameof(arguments)); - EnsureArg.IsNotNull(arguments.ChangeDataset, nameof(arguments.ChangeDataset)); - EnsureArg.IsNotNull(arguments.StudyInstanceUid, nameof(arguments.StudyInstanceUid)); - EnsureArg.IsNotNull(arguments.InstanceMetadataList, nameof(arguments.InstanceMetadataList)); - EnsureArg.IsNotNull(logger, nameof(logger)); - - logger.LogInformation("Completing updating operation for study."); - - IReadOnlyCollection queryTags = await _queryTagService.GetQueryTagsAsync(cancellationToken: CancellationToken.None); - - // Filter extended query tag at study level - var filteredQueryTags = queryTags.Where(x => x.IsExtendedQueryTag && UpdateTags.UpdateStudyFilterTags.Contains(x.Tag) && x.Level == QueryTagLevel.Study).ToList(); - - if (filteredQueryTags.Count > 0) - { - logger.LogInformation("Updating study with extended query tags. Extended query tag count {Count}", filteredQueryTags.Count); - } - - await _indexStore.EndUpdateInstanceAsync( - arguments.PartitionKey, - arguments.StudyInstanceUid, - GetDeserializedDataset(arguments.ChangeDataset), - arguments.InstanceMetadataList, - filteredQueryTags, - CancellationToken.None); - - logger.LogInformation("Updating study completed successfully."); - } - - /// - /// Asynchronously delete all the old blobs if it has more than 2 version. - /// - /// Activity context which has list of watermarks to cleanup - /// A diagnostic logger. - /// - /// A task representing the operation. - /// - /// - /// or is . - /// - [FunctionName(nameof(DeleteOldVersionBlobV3Async))] - public async Task DeleteOldVersionBlobV3Async([ActivityTrigger] CleanupBlobArgumentsV2 arguments, ILogger logger) - { - EnsureArg.IsNotNull(arguments, nameof(arguments)); - EnsureArg.IsNotNull(arguments.Partition, nameof(arguments.Partition)); - EnsureArg.IsNotNull(logger, nameof(logger)); - EnsureArg.IsNotNull(arguments.Instances, nameof(arguments.Instances)); - - IReadOnlyList instances = arguments.Instances; - Partition partition = arguments.Partition; - int fileCount = instances.Where(i => i.InstanceProperties.OriginalVersion.HasValue).Count(); - - logger.LogInformation("Begin deleting old blobs. Total size {TotalCount}", fileCount); - - await Parallel.ForEachAsync( - instances.Where(i => i.InstanceProperties.OriginalVersion.HasValue), - new ParallelOptions - { - CancellationToken = default, - MaxDegreeOfParallelism = _options.MaxParallelThreads, - }, - async (instance, token) => - { - await _updateInstanceService.DeleteInstanceBlobAsync(instance.VersionedInstanceIdentifier.Version, partition, instance.InstanceProperties.FileProperties, token); - }); - - logger.LogInformation("Old blobs deleted successfully. Total size {TotalCount}", fileCount); - } - - /// - /// Asynchronously delete the new blob when there is a failure while updating the study instances. - /// - /// arguments which have a list of watermarks to cleanup along with partition they belong to - /// A diagnostic logger. - /// - /// A task representing the operation. - /// - /// - /// or is . - /// - [FunctionName(nameof(CleanupNewVersionBlobV3Async))] - public async Task CleanupNewVersionBlobV3Async([ActivityTrigger] CleanupBlobArgumentsV2 arguments, ILogger logger) - { - EnsureArg.IsNotNull(arguments, nameof(arguments)); - EnsureArg.IsNotNull(arguments.Partition, nameof(arguments.Partition)); - EnsureArg.IsNotNull(logger, nameof(logger)); - EnsureArg.IsNotNull(arguments.Instances, nameof(arguments.Instances)); - - IReadOnlyList instances = arguments.Instances; - Partition partition = arguments.Partition; - - int fileCount = instances.Where(instance => instance.InstanceProperties.NewVersion.HasValue).Count(); - logger.LogInformation("Begin cleaning up new blobs. Total size {TotalCount}", fileCount); - - await Parallel.ForEachAsync( - instances.Where(instance => instance.InstanceProperties.NewVersion.HasValue), - new ParallelOptions - { - CancellationToken = default, - MaxDegreeOfParallelism = _options.MaxParallelThreads, - }, - async (instance, token) => - { - await _updateInstanceService.DeleteInstanceBlobAsync(instance.InstanceProperties.NewVersion.Value, partition, instance.InstanceProperties.FileProperties, token); - }); - - logger.LogInformation("New blobs deleted successfully. Total size {TotalCount}", fileCount); - } - - /// - /// Asynchronously move all the original version blobs to cold access tier. - /// - /// arguments which have a list of watermarks to move to cold access tier along with partition they belong to - /// A diagnostic logger. - /// - /// A task representing the operation. - /// - /// - /// or is . - /// - [FunctionName(nameof(SetOriginalBlobToColdAccessTierV2Async))] - public async Task SetOriginalBlobToColdAccessTierV2Async([ActivityTrigger] CleanupBlobArgumentsV2 arguments, ILogger logger) - { - EnsureArg.IsNotNull(arguments, nameof(arguments)); - EnsureArg.IsNotNull(arguments.Partition, nameof(arguments.Partition)); - EnsureArg.IsNotNull(logger, nameof(logger)); - - IReadOnlyList instances = arguments.Instances; - Partition partition = arguments.Partition; - - int fileCount = instances.Where(i => i.InstanceProperties.NewVersion.HasValue && !i.InstanceProperties.OriginalVersion.HasValue).Count(); - logger.LogInformation("Begin moving original version blob from hot to cold access tier. Total size {TotalCount}", fileCount); - - // Set to cold tier only for first time update, not for subsequent updates. This is to avoid moving the blob to cold tier multiple times. - // If the original version is set, then it means that the instance is updated already. - await Parallel.ForEachAsync( - instances.Where(i => i.InstanceProperties.NewVersion.HasValue && !i.InstanceProperties.OriginalVersion.HasValue), - new ParallelOptions - { - CancellationToken = default, - MaxDegreeOfParallelism = _options.MaxParallelThreads, - }, - async (instance, token) => - { - await _fileStore.SetBlobToColdAccessTierAsync(instance.VersionedInstanceIdentifier.Version, partition, instance.InstanceProperties.FileProperties, token); - }); - - logger.LogInformation("Original version blob is moved to cold access tier successfully. Total size {TotalCount}", fileCount); - } - - private static string ToInstanceString(VersionedInstanceIdentifier versionedInstanceIdentifier) - => $"PartitionKey: {versionedInstanceIdentifier.Partition.Name}, StudyInstanceUID: {versionedInstanceIdentifier.StudyInstanceUid}, SeriesInstanceUID: {versionedInstanceIdentifier.SeriesInstanceUid}, SOPInstanceUID: {versionedInstanceIdentifier.SopInstanceUid}"; - - private DicomDataset GetDeserializedDataset(string dataset) => JsonSerializer.Deserialize(dataset, _jsonSerializerOptions); -} diff --git a/src/Microsoft.Health.Dicom.Functions/Update/UpdateDurableFunction.Orchestration.cs b/src/Microsoft.Health.Dicom.Functions/Update/UpdateDurableFunction.Orchestration.cs deleted file mode 100644 index d9c319f7b3..0000000000 --- a/src/Microsoft.Health.Dicom.Functions/Update/UpdateDurableFunction.Orchestration.cs +++ /dev/null @@ -1,474 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Data; -using System.Diagnostics; -using System.Linq; -using System.Net; -using System.Text.Json; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Microsoft.Extensions.Logging; -using Microsoft.Health.Core.Features.Audit; -using Microsoft.Health.Dicom.Core.Features.Audit; -using Microsoft.Health.Dicom.Core.Features.Diagnostic; -using Microsoft.Health.Dicom.Core.Features.Model; -using Microsoft.Health.Dicom.Core.Features.Partitioning; -using Microsoft.Health.Dicom.Functions.Registration; -using Microsoft.Health.Dicom.Functions.Update.Models; -using Microsoft.Health.Operations.Functions.DurableTask; - -namespace Microsoft.Health.Dicom.Functions.Update; - -public partial class UpdateDurableFunction -{ - /// - /// Asynchronously updates list of instances in a study - /// - /// - /// Durable functions are reliable, and their implementations will be executed repeatedly over the lifetime of - /// a single instance. - /// - /// The context for the orchestration instance. - /// A diagnostic logger. - /// A task representing the operation. - /// - /// or is . - /// - /// Orchestration instance ID is invalid. - [FunctionName(nameof(UpdateInstancesV5Async))] - [Obsolete("This function is obsolete. Use UpdateInstancesV6Async instead.")] - public async Task UpdateInstancesV5Async( - [OrchestrationTrigger] IDurableOrchestrationContext context, - ILogger logger) - { - EnsureArg.IsNotNull(context, nameof(context)).ThrowIfInvalidOperationId(); - logger = context.CreateReplaySafeLogger(EnsureArg.IsNotNull(logger, nameof(logger))); - ReplaySafeCounter replaySafeCounter = context.CreateReplaySafeCounter(_updateMeter.UpdatedInstances); - IReadOnlyList instanceMetadataList; - UpdateCheckpoint input = context.GetInput(); - input.Partition ??= new Partition(input.PartitionKey, Partition.UnknownName); - - _auditLogger.LogAudit( - AuditAction.Executing, - AuditEventSubType.UpdateStudyOperation, - null, - null, - Activity.Current?.RootId, - null, - null, - null); - - if (input.NumberOfStudyCompleted < input.TotalNumberOfStudies) - { - string studyInstanceUid = input.StudyInstanceUids[input.NumberOfStudyCompleted]; - - logger.LogInformation("Beginning to update all instances new watermark in a study."); - - IReadOnlyList instances = await context - .CallActivityWithRetryAsync>( - nameof(UpdateInstanceWatermarkV2Async), - _options.RetryOptions, - new UpdateInstanceWatermarkArgumentsV2(input.Partition, studyInstanceUid)); - var instanceWatermarks = instances.Select(x => x.ToInstanceFileState()).ToList(); - - logger.LogInformation("Updated all instances new watermark in a study. Found {InstanceCount} instance for study", instances.Count); - - var totalNoOfInstances = input.TotalNumberOfInstanceUpdated; - - if (instances.Count > 0) - { - bool isFailedToUpdateStudy = false; - - try - { - UpdateInstanceResponse response = await context.CallActivityWithRetryAsync( - nameof(UpdateInstanceBlobsV3Async), - _options.RetryOptions, - new UpdateInstanceBlobArgumentsV2(input.Partition, instances, input.ChangeDataset)); - - instanceMetadataList = response.InstanceMetadataList; - - if (response.Errors?.Count > 0) - { - isFailedToUpdateStudy = true; - logger.LogWarning("Failed to update instances for study. Total instance failed for study {TotalFailed}", response.Errors.Count); - await HandleException(context, input, studyInstanceUid, instances, response.Errors); - } - else - { - await context.CallActivityWithRetryAsync( - nameof(CompleteUpdateStudyV4Async), - _options.RetryOptions, - new CompleteStudyArgumentsV2(input.Partition.Key, studyInstanceUid, input.ChangeDataset, GetInstanceMetadataList(instanceMetadataList))); - - totalNoOfInstances += instances.Count; - } - } - catch (FunctionFailedException ex) - { - isFailedToUpdateStudy = true; - - logger.LogError(ex, "Failed to update instances for study", ex); - - await HandleException(context, input, studyInstanceUid, instances, null); - } - - if (!isFailedToUpdateStudy) - { - await context.CallActivityWithRetryAsync( - nameof(DeleteOldVersionBlobV3Async), - _options.RetryOptions, - new CleanupBlobArgumentsV2(instances, input.Partition)); - - await context.CallActivityWithRetryAsync( - nameof(SetOriginalBlobToColdAccessTierV2Async), - _options.RetryOptions, - new CleanupBlobArgumentsV2(instances, input.Partition)); - } - } - - var numberOfStudyCompleted = input.NumberOfStudyCompleted + 1; - - if (input.TotalNumberOfStudies != numberOfStudyCompleted) - { - logger.LogInformation("Completed updating the instances for a study. {Updated}. Continuing with new execution...", instances.Count); - } - - context.ContinueAsNew( - new UpdateCheckpoint - { - StudyInstanceUids = input.StudyInstanceUids, - ChangeDataset = input.ChangeDataset, - Partition = input.Partition, - PartitionKey = input.PartitionKey, - NumberOfStudyCompleted = numberOfStudyCompleted, - NumberOfStudyFailed = input.NumberOfStudyFailed, - TotalNumberOfInstanceUpdated = totalNoOfInstances, - Errors = input.Errors, - CreatedTime = input.CreatedTime ?? await context.GetCreatedTimeAsync(_options.RetryOptions), - }); - } - else - { - if (input.TotalNumberOfInstanceUpdated > 0) - { - replaySafeCounter.Add(input.TotalNumberOfInstanceUpdated); - } - - string serializedInput = GetSerializedCheckpointResult(input); - - if (input.Errors?.Count > 0) - { - logger.LogWarning("Update operation completed with errors. {NumberOfStudyUpdated}, {NumberOfStudyFailed}, {TotalNumberOfInstanceUpdated}.", - input.NumberOfStudyCompleted - input.NumberOfStudyFailed, - input.NumberOfStudyFailed, - input.TotalNumberOfInstanceUpdated); - - _telemetryClient.ForwardOperationLogTrace( - "Update operation completed with errors", - context.InstanceId, - serializedInput, - AuditEventSubType.UpdateStudyOperation, - ApplicationInsights.DataContracts.SeverityLevel.Error); - - _auditLogger.LogAudit( - AuditAction.Executed, - AuditEventSubType.UpdateStudyOperation, - null, - HttpStatusCode.BadRequest, - Activity.Current?.RootId, - null, - null, - null); - - // Throwing the exception so that it can set the operation status to Failed - throw new OperationErrorException("Update operation completed with errors."); - } - else - { - logger.LogInformation("Update operation completed successfully. {NumberOfStudyUpdated}, {TotalNumberOfInstanceUpdated}.", - input.NumberOfStudyCompleted, - input.TotalNumberOfInstanceUpdated); - - _telemetryClient.ForwardOperationLogTrace("Update operation completed successfully", context.InstanceId, serializedInput, AuditEventSubType.UpdateStudyOperation); - - _auditLogger.LogAudit( - AuditAction.Executed, - AuditEventSubType.UpdateStudyOperation, - null, - HttpStatusCode.OK, - Activity.Current?.RootId, - null, - null, - null); - } - } - } - - /// - /// Asynchronously updates list of instances in a study - /// - /// - /// Durable functions are reliable, and their implementations will be executed repeatedly over the lifetime of - /// a single instance. - /// - /// The context for the orchestration instance. - /// A diagnostic logger. - /// A task representing the operation. - /// - /// or is . - /// - /// Orchestration instance ID is invalid. - [FunctionName(nameof(UpdateInstancesV6Async))] - public async Task UpdateInstancesV6Async( - [OrchestrationTrigger] IDurableOrchestrationContext context, - ILogger logger) - { - EnsureArg.IsNotNull(context, nameof(context)).ThrowIfInvalidOperationId(); - logger = context.CreateReplaySafeLogger(EnsureArg.IsNotNull(logger, nameof(logger))); - ReplaySafeCounter replaySafeCounter = context.CreateReplaySafeCounter(_updateMeter.UpdatedInstances); - IReadOnlyList instanceMetadataList; - UpdateCheckpoint input = context.GetInput(); - input.Partition ??= new Partition(input.PartitionKey, Partition.UnknownName); - - _auditLogger.LogAudit( - AuditAction.Executing, - AuditEventSubType.UpdateStudyOperation, - null, - null, - Activity.Current?.RootId, - null, - null, - null); - - if (input.NumberOfStudyProcessed < input.TotalNumberOfStudies) - { - string studyInstanceUid = input.StudyInstanceUids[input.NumberOfStudyProcessed]; - - logger.LogInformation("Beginning to update all instances new watermark in a study."); - - IReadOnlyList instances = await context - .CallActivityWithRetryAsync>( - nameof(UpdateInstanceWatermarkV2Async), - _options.RetryOptions, - new UpdateInstanceWatermarkArgumentsV2(input.Partition, studyInstanceUid)); - var instanceWatermarks = instances.Select(x => x.ToInstanceFileState()).ToList(); - - logger.LogInformation("Updated all instances new watermark in a study. Found {InstanceCount} instance for study", instances.Count); - - int totalNoOfInstances = input.TotalNumberOfInstanceUpdated; - int numberStudyUpdated = input.NumberOfStudyCompleted; - - if (instances.Count > 0) - { - bool isFailedToUpdateStudy = false; - - try - { - UpdateInstanceResponse response = await context.CallActivityWithRetryAsync( - nameof(UpdateInstanceBlobsV3Async), - _options.RetryOptions, - new UpdateInstanceBlobArgumentsV2(input.Partition, instances, input.ChangeDataset)); - - instanceMetadataList = response.InstanceMetadataList; - - if (response.Errors?.Count > 0) - { - isFailedToUpdateStudy = true; - logger.LogWarning("Failed to update instances for study. Total instance failed for study {TotalFailed}", response.Errors.Count); - await HandleException(context, input, studyInstanceUid, instances, response.Errors); - } - else - { - await context.CallActivityWithRetryAsync( - nameof(CompleteUpdateStudyV4Async), - _options.RetryOptions, - new CompleteStudyArgumentsV2(input.Partition.Key, studyInstanceUid, input.ChangeDataset, GetInstanceMetadataList(instanceMetadataList))); - - totalNoOfInstances += instances.Count; - numberStudyUpdated++; - } - } - catch (FunctionFailedException ex) - { - isFailedToUpdateStudy = true; - - logger.LogError(ex, "Failed to update instances for study", ex); - - await HandleException(context, input, studyInstanceUid, instances, null); - } - - if (!isFailedToUpdateStudy) - { - await context.CallActivityWithRetryAsync( - nameof(DeleteOldVersionBlobV3Async), - _options.RetryOptions, - new CleanupBlobArgumentsV2(instances, input.Partition)); - - await context.CallActivityWithRetryAsync( - nameof(SetOriginalBlobToColdAccessTierV2Async), - _options.RetryOptions, - new CleanupBlobArgumentsV2(instances, input.Partition)); - } - } - - var numberOfStudyProcessed = input.NumberOfStudyProcessed + 1; - - if (input.TotalNumberOfStudies != numberOfStudyProcessed) - { - logger.LogInformation("Completed updating the instances for a study. {Updated}. Continuing with new execution...", instances.Count); - } - - context.ContinueAsNew( - new UpdateCheckpoint - { - StudyInstanceUids = input.StudyInstanceUids, - ChangeDataset = input.ChangeDataset, - Partition = input.Partition, - PartitionKey = input.PartitionKey, - NumberOfStudyProcessed = numberOfStudyProcessed, - NumberOfStudyCompleted = numberStudyUpdated, - NumberOfStudyFailed = input.NumberOfStudyFailed, - TotalNumberOfInstanceUpdated = totalNoOfInstances, - Errors = input.Errors, - CreatedTime = input.CreatedTime ?? await context.GetCreatedTimeAsync(_options.RetryOptions), - }); - } - else - { - if (input.TotalNumberOfInstanceUpdated > 0) - { - replaySafeCounter.Add(input.TotalNumberOfInstanceUpdated); - } - - string serializedInput = GetSerializedCheckpointResult(input); - - if (input.Errors?.Count > 0) - { - logger.LogWarning("Update operation completed with errors. {NumberOfStudyProcessed}, {NumberOfStudyUpdated}, {NumberOfStudyFailed}, {TotalNumberOfInstanceUpdated}.", - input.NumberOfStudyProcessed, - input.NumberOfStudyCompleted, - input.NumberOfStudyFailed, - input.TotalNumberOfInstanceUpdated); - - _telemetryClient.ForwardOperationLogTrace( - "Update operation completed with errors", - context.InstanceId, - serializedInput, - AuditEventSubType.UpdateStudyOperation, - ApplicationInsights.DataContracts.SeverityLevel.Error); - - _auditLogger.LogAudit( - AuditAction.Executed, - AuditEventSubType.UpdateStudyOperation, - null, - HttpStatusCode.BadRequest, - Activity.Current?.RootId, - null, - null, - null); - - // Throwing the exception so that it can set the operation status to Failed - throw new OperationErrorException("Update operation completed with errors."); - } - else - { - logger.LogInformation("Update operation completed successfully. {NumberOfStudyProcessed}, {NumberOfStudyUpdated}, {TotalNumberOfInstanceUpdated}.", - input.NumberOfStudyProcessed, - input.NumberOfStudyCompleted, - input.TotalNumberOfInstanceUpdated); - - _telemetryClient.ForwardOperationLogTrace("Update operation completed successfully", context.InstanceId, serializedInput, AuditEventSubType.UpdateStudyOperation); - - _auditLogger.LogAudit( - AuditAction.Executed, - AuditEventSubType.UpdateStudyOperation, - null, - HttpStatusCode.OK, - Activity.Current?.RootId, - null, - null, - null); - } - } - } - - private async Task HandleException( - IDurableOrchestrationContext context, - UpdateCheckpoint input, - string studyInstanceUid, - IReadOnlyList instances, - IReadOnlyList instanceErrors) - { - var errors = new List(); - - if (input.Errors != null) - { - errors.AddRange(input.Errors); - } - - errors.Add($"Failed to update instances for study {studyInstanceUid}"); - - if (instanceErrors != null) - { - // We don't want to populate all the errors in Azure Table Storage, DTFx may attempt to compress the entry as needed using GZip and storing in blob storage - // But I think we should also be wary of what the user experience is for this via the response, so restricting to 5 errors for now. We can update based on feedback. - errors.AddRange(instanceErrors.Take(5)); - - if (instanceErrors.Count > 5) - { - errors.Add("There are more instances failed to update than listed above. Please check the diagnostics logs for the complete list."); - } - - foreach (string error in instanceErrors) - { - _telemetryClient.ForwardOperationLogTrace(error, context.InstanceId, string.Empty, AuditEventSubType.UpdateStudyOperation, ApplicationInsights.DataContracts.SeverityLevel.Error); - } - } - - input.Errors = errors; - input.NumberOfStudyFailed++; - - // Cleanup the new version when the update activity fails - await TryCleanupActivityV3(context, instances, input.Partition); - } - - private IReadOnlyList GetInstanceMetadataList(IReadOnlyList instanceMetadataList) - { - // when external store not enabled, do not update file properties - return _externalStoreEnabled ? instanceMetadataList : new List(); - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Using a generic exception to catch all scenarios.")] - private async Task TryCleanupActivityV3(IDurableOrchestrationContext context, IReadOnlyList instances, Partition partition) - { - try - { - await context.CallActivityWithRetryAsync( - nameof(CleanupNewVersionBlobV3Async), - _options.RetryOptions, - new CleanupBlobArgumentsV2(instances, partition)); - } - catch (Exception) { } - } - - private string GetSerializedCheckpointResult(UpdateCheckpoint checkpoint) - { - return JsonSerializer.Serialize(new - { - checkpoint.StudyInstanceUids, - partitionName = checkpoint.Partition.Name, - datasetToUpdate = checkpoint.ChangeDataset, - checkpoint.NumberOfStudyCompleted, - checkpoint.NumberOfStudyFailed, - checkpoint.TotalNumberOfInstanceUpdated, - }, _jsonSerializerOptions); - } -} diff --git a/src/Microsoft.Health.Dicom.Functions/Update/UpdateDurableFunction.cs b/src/Microsoft.Health.Dicom.Functions/Update/UpdateDurableFunction.cs deleted file mode 100644 index ea0014f9a9..0000000000 --- a/src/Microsoft.Health.Dicom.Functions/Update/UpdateDurableFunction.cs +++ /dev/null @@ -1,67 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Text.Json; -using EnsureThat; -using Microsoft.ApplicationInsights; -using Microsoft.Extensions.Options; -using Microsoft.Health.Dicom.Core.Configs; -using Microsoft.Health.Dicom.Core.Features.Audit; -using Microsoft.Health.Dicom.Core.Features.Common; -using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag; -using Microsoft.Health.Dicom.Core.Features.Retrieve; -using Microsoft.Health.Dicom.Core.Features.Store; -using Microsoft.Health.Dicom.Core.Features.Telemetry; -using Microsoft.Health.Dicom.Core.Features.Update; - -namespace Microsoft.Health.Dicom.Functions.Update; - -/// -/// Represents the Azure Durable Functions that perform updating list of instances in multiple studies. -/// -public partial class UpdateDurableFunction -{ - private readonly IIndexDataStore _indexStore; - private readonly IInstanceStore _instanceStore; - private readonly UpdateOptions _options; - private readonly IMetadataStore _metadataStore; - private readonly IFileStore _fileStore; - private readonly IUpdateInstanceService _updateInstanceService; - private readonly IQueryTagService _queryTagService; - private readonly UpdateMeter _updateMeter; - private readonly TelemetryClient _telemetryClient; - private readonly IAuditLogger _auditLogger; - private readonly JsonSerializerOptions _jsonSerializerOptions; - private readonly bool _externalStoreEnabled; - - public UpdateDurableFunction( - IIndexDataStore indexStore, - IInstanceStore instanceStore, - IOptions configOptions, - IMetadataStore metadataStore, - IFileStore fileStore, - IUpdateInstanceService updateInstanceService, - IQueryTagService queryTagService, - UpdateMeter updateMeter, - TelemetryClient telemetryClient, - IAuditLogger auditLogger, - IOptions jsonSerializerOptions, - IOptions featureConfiguration) - { - EnsureArg.IsNotNull(featureConfiguration, nameof(featureConfiguration)); - _indexStore = EnsureArg.IsNotNull(indexStore, nameof(indexStore)); - _instanceStore = EnsureArg.IsNotNull(instanceStore, nameof(instanceStore)); - _metadataStore = EnsureArg.IsNotNull(metadataStore, nameof(metadataStore)); - _fileStore = EnsureArg.IsNotNull(fileStore, nameof(fileStore)); - _updateInstanceService = EnsureArg.IsNotNull(updateInstanceService, nameof(updateInstanceService)); - _queryTagService = EnsureArg.IsNotNull(queryTagService, nameof(queryTagService)); - _jsonSerializerOptions = EnsureArg.IsNotNull(jsonSerializerOptions?.Value, nameof(jsonSerializerOptions)); - _updateMeter = EnsureArg.IsNotNull(updateMeter, nameof(updateMeter)); - _telemetryClient = EnsureArg.IsNotNull(telemetryClient, nameof(telemetryClient)); - _auditLogger = EnsureArg.IsNotNull(auditLogger, nameof(auditLogger)); - _options = EnsureArg.IsNotNull(configOptions?.Value, nameof(configOptions)); - _externalStoreEnabled = featureConfiguration.Value.EnableExternalStore; - } -} diff --git a/src/Microsoft.Health.Dicom.Functions/Update/UpdateOptions.cs b/src/Microsoft.Health.Dicom.Functions/Update/UpdateOptions.cs deleted file mode 100644 index 3966e6841f..0000000000 --- a/src/Microsoft.Health.Dicom.Functions/Update/UpdateOptions.cs +++ /dev/null @@ -1,34 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.ComponentModel.DataAnnotations; -using Microsoft.Health.Operations.Functions.DurableTask; - -namespace Microsoft.Health.Dicom.Functions.Update; - -/// -/// Represents the options for a "update" function. -/// -public class UpdateOptions -{ - internal const string SectionName = "Update"; - - /// - /// Gets or sets the number of DICOM instances updated in a single batch inside a activity. - /// - public int BatchSize { get; set; } = 100; - - /// - /// Gets or sets the number of threads available for each batch. - /// - [Range(-1, int.MaxValue)] - public int MaxParallelThreads { get; set; } = -1; - - /// - /// Gets or sets the for update activities. - /// - public ActivityRetryOptions RetryOptions { get; set; } -} diff --git a/src/Microsoft.Health.Dicom.SchemaManager.Console/Microsoft.Health.Dicom.SchemaManager.Console.csproj b/src/Microsoft.Health.Dicom.SchemaManager.Console/Microsoft.Health.Dicom.SchemaManager.Console.csproj deleted file mode 100644 index 89c89334c5..0000000000 --- a/src/Microsoft.Health.Dicom.SchemaManager.Console/Microsoft.Health.Dicom.SchemaManager.Console.csproj +++ /dev/null @@ -1,26 +0,0 @@ - - - - enable - Exe - $(LatestVersion) - - - - - - - - - - - - - - - - PreserveNewest - - - - diff --git a/src/Microsoft.Health.Dicom.SchemaManager.Console/Program.cs b/src/Microsoft.Health.Dicom.SchemaManager.Console/Program.cs deleted file mode 100644 index 88ee5d9d97..0000000000 --- a/src/Microsoft.Health.Dicom.SchemaManager.Console/Program.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.CommandLine.Parsing; -using System.Threading.Tasks; -using Microsoft.Extensions.Hosting; -using Microsoft.Health.Dicom.SchemaManager; - -internal static class Program -{ - public static Task Main(string[] args) - { - IHost host = Host.CreateDefaultBuilder() - .ConfigureAppConfiguration(builder => builder.AddSchemaCommandLine(args)) - .ConfigureServices((context, collection) => collection.AddSchemaManager(context.Configuration)) - .Build(); - - return SchemaManagerParser - .Build(host.Services) - .InvokeAsync(args); - } -} diff --git a/src/Microsoft.Health.Dicom.SchemaManager.Console/Properties/AssemblyInfo.cs b/src/Microsoft.Health.Dicom.SchemaManager.Console/Properties/AssemblyInfo.cs deleted file mode 100644 index 2e456f0a71..0000000000 --- a/src/Microsoft.Health.Dicom.SchemaManager.Console/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,10 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Resources; - -[assembly: NeutralResourcesLanguage("en-us")] -[assembly: CLSCompliant(false)] diff --git a/src/Microsoft.Health.Dicom.SchemaManager.Console/Properties/launchSettings.json b/src/Microsoft.Health.Dicom.SchemaManager.Console/Properties/launchSettings.json deleted file mode 100644 index 0e50866ebf..0000000000 --- a/src/Microsoft.Health.Dicom.SchemaManager.Console/Properties/launchSettings.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "profiles": { - "Microsoft.Health.Dicom.SchemaManager": { - "commandName": "Project", - "commandLineArgs": "apply --connection-string \"server=(local);Initial Catalog=DICOM3;TrustServerCertificate=True;Integrated Security=True\" --version 20 -wi=false", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } - } -} diff --git a/src/Microsoft.Health.Dicom.SchemaManager.Console/appsettings.json b/src/Microsoft.Health.Dicom.SchemaManager.Console/appsettings.json deleted file mode 100644 index e203e9407e..0000000000 --- a/src/Microsoft.Health.Dicom.SchemaManager.Console/appsettings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Debug", - "System": "Information", - "Microsoft": "Information" - } - } -} diff --git a/src/Microsoft.Health.Dicom.SchemaManager.UnitTests/DicomSchemaClientTests.cs b/src/Microsoft.Health.Dicom.SchemaManager.UnitTests/DicomSchemaClientTests.cs deleted file mode 100644 index e0631e57a3..0000000000 --- a/src/Microsoft.Health.Dicom.SchemaManager.UnitTests/DicomSchemaClientTests.cs +++ /dev/null @@ -1,98 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Health.Dicom.SqlServer.Features.Schema; -using Microsoft.Health.SqlServer.Features.Schema; -using Microsoft.Health.SqlServer.Features.Schema.Manager; -using Microsoft.Health.SqlServer.Features.Schema.Manager.Model; -using NSubstitute; -using Xunit; - -namespace Microsoft.Health.Dicom.SchemaManager.UnitTests; - -public class DicomSchemaClientTests -{ - private readonly IScriptProvider _scriptProvider = Substitute.For(); - private readonly ISchemaDataStore _schemaDataStore = Substitute.For(); - private readonly ISchemaManagerDataStore _schemaManagerDataStore = Substitute.For(); - - [Fact] - public async Task GivenCurrentVersionAboveOne_GetAvailableVersions_ShouldReturnCorrectVersionsAsync() - { - //Arrange - int currentVersion = 5; - _schemaManagerDataStore.GetCurrentSchemaVersionAsync(default).Returns(currentVersion); - - var dicomSchemaClient = new DicomSchemaClient(_scriptProvider, _schemaDataStore, _schemaManagerDataStore); - - //Act - List? actualVersions = await dicomSchemaClient.GetAvailabilityAsync(); - - int numberOfAvailableVersions = SchemaVersionConstants.Max - currentVersion + 1; - var expectedVersions = Enumerable - .Range(currentVersion, numberOfAvailableVersions) - .Select(version => new AvailableVersion(version, $"{version}.sql", $"{version}.diff.sql")) - .ToList(); - - //Assert - Assert.Equal(expectedVersions, actualVersions, new AvailableVersionEqualityCompare()); - } - - [Fact] - public async Task GivenCurrentVersionOfMax_GetAvailableVersionsShouldReturnOneVersion() - { - //Arrange - _schemaManagerDataStore.GetCurrentSchemaVersionAsync(default).Returns(SchemaVersionConstants.Max); - var dicomSchemaClient = new DicomSchemaClient(_scriptProvider, _schemaDataStore, _schemaManagerDataStore); - - //Act - List? actualVersions = await dicomSchemaClient.GetAvailabilityAsync(); - - var expectedVersions = new List() - { - new AvailableVersion(SchemaVersionConstants.Max, $"{SchemaVersionConstants.Max}.sql", $"{SchemaVersionConstants.Max}.diff.sql"), - }; - - //Assert - Assert.Equal(expectedVersions, actualVersions, new AvailableVersionEqualityCompare()); - } - - private class AvailableVersionEqualityCompare : IEqualityComparer - { - public bool Equals(AvailableVersion? x, AvailableVersion? y) - { - if (ReferenceEquals(x, y)) - { - return true; - } - - if (x is null) - { - return false; - } - - if (y is null) - { - return false; - } - - if (x.GetType() != y.GetType()) - { - return false; - } - - return x.Id == y.Id && x.ScriptUri == y.ScriptUri && x.DiffUri == y.DiffUri; - } - - public int GetHashCode(AvailableVersion obj) - { - return HashCode.Combine(obj.Id, obj.ScriptUri, obj.DiffUri); - } - } -} diff --git a/src/Microsoft.Health.Dicom.SchemaManager.UnitTests/Microsoft.Health.Dicom.SchemaManager.UnitTests.csproj b/src/Microsoft.Health.Dicom.SchemaManager.UnitTests/Microsoft.Health.Dicom.SchemaManager.UnitTests.csproj deleted file mode 100644 index 08fbbdc703..0000000000 --- a/src/Microsoft.Health.Dicom.SchemaManager.UnitTests/Microsoft.Health.Dicom.SchemaManager.UnitTests.csproj +++ /dev/null @@ -1,22 +0,0 @@ - - - - enable - $(LatestVersion) - - - - - - - - - - - - - - - - - diff --git a/src/Microsoft.Health.Dicom.SchemaManager/ApplyCommand.cs b/src/Microsoft.Health.Dicom.SchemaManager/ApplyCommand.cs deleted file mode 100644 index cab8204c0a..0000000000 --- a/src/Microsoft.Health.Dicom.SchemaManager/ApplyCommand.cs +++ /dev/null @@ -1,68 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.CommandLine; -using System.CommandLine.NamingConventionBinder; -using System.Diagnostics.CodeAnalysis; -using System.Threading; -using System.Threading.Tasks; -using EnsureThat; -using Microsoft.Extensions.Logging; -using Microsoft.Health.SqlServer.Features.Schema.Manager; -using Microsoft.Health.SqlServer.Features.Schema.Manager.Model; - -namespace Microsoft.Health.Dicom.SchemaManager; - -[SuppressMessage("Naming", "CA1710: Identifiers should have correct suffix", Justification = "Base class is also called Command.")] -public class ApplyCommand : Command -{ - private readonly ISchemaManager _schemaManager; - private readonly ILogger _logger; - - public ApplyCommand( - ISchemaManager schemaManager, - ILogger logger) - : base(CommandNames.Apply, Resources.ApplyCommandDescription) - { - EnsureArg.IsNotNull(logger, nameof(logger)); - EnsureArg.IsNotNull(schemaManager, nameof(schemaManager)); - - AddOption(CommandOptions.ConnectionStringOption()); - AddOption(CommandOptions.ManagedIdentityClientIdOption()); - AddOption(CommandOptions.AuthenticationTypeOption()); - AddOption(CommandOptions.EnableWorkloadIdentityOptions()); - AddOption(CommandOptions.VersionOption()); - AddOption(CommandOptions.NextOption()); - AddOption(CommandOptions.LatestOption()); - AddOption(CommandOptions.ForceOption()); - - AddValidator(commandResult => MutuallyExclusiveOptionValidator.Validate(commandResult, new List