Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions docs/rest/customSearchExample.http
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,57 @@ Authorization: Bearer {{bearer.response.body.access_token}}
### Same as above but reversed.
GET https://{{hostname}}/Patient?CreatedDate:missing=false&_sort=-CreatedDate
Authorization: Bearer {{bearer.response.body.access_token}}


### Try to update an out of the box search param
PUT https://{{hostname}}/SearchParameter/AllergyIntolerance-clinical-status
content-type: application/json
Authorization: Bearer {{bearer.response.body.access_token}}

{
"resourceType": "SearchParameter",
"id": "AllergyIntolerance-clinical-status",
"url": "http://hl7.org/fhir/SearchParameter/AllergyIntolerance-clinical-status",
"version": "4.0.1",
"name": "clinical-status",
"status": "active",
"experimental": false,
"date": "2019-11-01T09:29:23+11:00",
"publisher": "Health Level Seven International (Patient Care)",
"contact": [
{
"telecom": [
{
"system": "url",
"value": "http://hl7.org/fhir"
}
]
}
],
"description": "active | inactive | resolved",
"code": "clinical-status",
"base": [
"AllergyIntolerance"
],
"type": "token",
"expression": "AllergyIntolerance.clinicalStatus",
"xpath": "f:AllergyIntolerance/f:clinicalStatus",
"xpathUsage": "normal",
"multipleOr": true,
"multipleAnd": true
}

### Try to delete an out of the box search param
DELETE https://{{hostname}}/SearchParameter/AllergyIntolerance-clinical-status
content-type: application/json
Authorization: Bearer {{bearer.response.body.access_token}}

### Delete custom search param example.
DELETE https://{{hostname}}/SearchParameter/resource-created-date2
content-type: application/json
Authorization: Bearer {{bearer.response.body.access_token}}

### Delete non-existent search param example.
DELETE https://{{hostname}}/SearchParameter/unknown
content-type: application/json
Authorization: Bearer {{bearer.response.body.access_token}}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@
// -------------------------------------------------------------------------------------------------

using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using EnsureThat;
using MediatR;
using Microsoft.Health.Fhir.Core.Exceptions;
using Microsoft.Health.Fhir.Core.Features.Definition;
using Microsoft.Health.Fhir.Core.Features.Persistence;
using Microsoft.Health.Fhir.Core.Messages.Delete;
using Microsoft.Health.Fhir.Core.Models;
Expand All @@ -17,33 +20,63 @@ namespace Microsoft.Health.Fhir.Core.Features.Search.Parameters
public class DeleteSearchParameterBehavior<TDeleteResourceRequest, TDeleteResourceResponse> : IPipelineBehavior<TDeleteResourceRequest, TDeleteResourceResponse>
where TDeleteResourceRequest : DeleteResourceRequest, IRequest<TDeleteResourceResponse>
{
private ISearchParameterOperations _searchParameterOperations;
private IFhirDataStore _fhirDataStore;
private readonly ISearchParameterOperations _searchParameterOperations;
private readonly IFhirDataStore _fhirDataStore;
private readonly ISearchParameterDefinitionManager _searchParameterDefinitionManager;

public DeleteSearchParameterBehavior(ISearchParameterOperations searchParameterOperations, IFhirDataStore fhirDataStore)
public DeleteSearchParameterBehavior(
ISearchParameterOperations searchParameterOperations,
IFhirDataStore fhirDataStore,
ISearchParameterDefinitionManager searchParameterDefinitionManager)
{
EnsureArg.IsNotNull(searchParameterOperations, nameof(searchParameterOperations));
EnsureArg.IsNotNull(fhirDataStore, nameof(fhirDataStore));
EnsureArg.IsNotNull(searchParameterDefinitionManager, nameof(searchParameterDefinitionManager));

_searchParameterOperations = searchParameterOperations;
_fhirDataStore = fhirDataStore;
_searchParameterDefinitionManager = searchParameterDefinitionManager;
}

public async Task<TDeleteResourceResponse> Handle(TDeleteResourceRequest request, RequestHandlerDelegate<TDeleteResourceResponse> next, CancellationToken cancellationToken)
{
var deleteRequest = request as DeleteResourceRequest;
ResourceWrapper searchParamResource = null;
ResourceWrapper customSearchParamResource = null;

if (deleteRequest.ResourceKey.ResourceType.Equals(KnownResourceTypes.SearchParameter, StringComparison.Ordinal))
{
searchParamResource = await _fhirDataStore.GetAsync(deleteRequest.ResourceKey, cancellationToken);
customSearchParamResource = await _fhirDataStore.GetAsync(deleteRequest.ResourceKey, cancellationToken);
Copy link
Contributor

@jestradaMS jestradaMS Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed, I am concerned with this approach as users could in theory create a param as "http://hl7.org/fhir/SearchParameter/" then perform a hard delete which removes from the table, but leaves it in PendingDelete and therefore it would have remained in def manager and not in resource table.

To be more accurate, it might be better to use our JSON file that provides or Spec Defined parameters and if found in that file, we throw method not allowed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will abandon this PR and create a new one with these changes.


// Check if this is a spec-defined SearchParameter (exists in definition manager but not in Resource table)
if (customSearchParamResource == null)
{
// Try to find by URL constructed from the resource ID
// Spec-defined parameters typically have URLs like: http://hl7.org/fhir/SearchParameter/{id}
var possibleUrl = $"http://hl7.org/fhir/SearchParameter/{deleteRequest.ResourceKey.Id}";
if (_searchParameterDefinitionManager.TryGetSearchParameter(possibleUrl, out var searchParameterInfo))
{
// Found in definition manager but not in Resource table - this is a spec-defined parameter
throw new MethodNotAllowedException(string.Format(Core.Resources.SearchParameterDefinitionCannotDeleteSpecDefined, possibleUrl));
}

// Also check all search parameters to see if any match this ID
var matchingParam = _searchParameterDefinitionManager.AllSearchParameters
.FirstOrDefault(sp => sp.Url.ToString().EndsWith($"/{deleteRequest.ResourceKey.Id}", StringComparison.OrdinalIgnoreCase));

if (matchingParam != null)
{
throw new MethodNotAllowedException(string.Format(Core.Resources.SearchParameterDefinitionCannotDeleteSpecDefined, matchingParam.Url));
}

// Not found in Resource table or definition manager - doesn't exist
throw new ResourceNotFoundException(string.Format(Core.Resources.ResourceNotFoundById, deleteRequest.ResourceKey.ResourceType, deleteRequest.ResourceKey.Id));
}
}

if (searchParamResource != null && searchParamResource.IsDeleted == false)
if (customSearchParamResource != null && customSearchParamResource.IsDeleted == false)

Check notice

Code scanning / CodeQL

Unnecessarily complex Boolean expression

The expression 'A == false' can be simplified to '!A'.

Copilot Autofix

AI 4 days ago

To fix the unnecessarily complex boolean expression, replace the comparison customSearchParamResource.IsDeleted == false with its simpler, equivalent form !customSearchParamResource.IsDeleted. This change should be made at line 76 of the file src/Microsoft.Health.Fhir.Core/Features/Search/Parameters/DeleteSearchParameterBehavior.cs. No additional methods, imports, or supporting changes are required, as the replacement uses intrinsic language operators. The fix is both locally correct and consistent with codebase conventions.


Suggested changeset 1
src/Microsoft.Health.Fhir.Core/Features/Search/Parameters/DeleteSearchParameterBehavior.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/Microsoft.Health.Fhir.Core/Features/Search/Parameters/DeleteSearchParameterBehavior.cs b/src/Microsoft.Health.Fhir.Core/Features/Search/Parameters/DeleteSearchParameterBehavior.cs
--- a/src/Microsoft.Health.Fhir.Core/Features/Search/Parameters/DeleteSearchParameterBehavior.cs
+++ b/src/Microsoft.Health.Fhir.Core/Features/Search/Parameters/DeleteSearchParameterBehavior.cs
@@ -73,7 +73,7 @@
                 }
             }
 
-            if (customSearchParamResource != null && customSearchParamResource.IsDeleted == false)
+            if (customSearchParamResource != null && !customSearchParamResource.IsDeleted)
             {
                 // This is a custom search parameter
                 await _searchParameterOperations.DeleteSearchParameterAsync(customSearchParamResource.RawResource, cancellationToken);
EOF
@@ -73,7 +73,7 @@
}
}

if (customSearchParamResource != null && customSearchParamResource.IsDeleted == false)
if (customSearchParamResource != null && !customSearchParamResource.IsDeleted)
{
// This is a custom search parameter
await _searchParameterOperations.DeleteSearchParameterAsync(customSearchParamResource.RawResource, cancellationToken);
Copilot is powered by AI and may make mistakes. Always verify output.
{
// First update the in-memory SearchParameterDefinitionManager, and remove the status metadata from the data store
// then remove the SearchParameter resource from the data store
await _searchParameterOperations.DeleteSearchParameterAsync(searchParamResource.RawResource, cancellationToken);
// This is a custom search parameter
await _searchParameterOperations.DeleteSearchParameterAsync(customSearchParamResource.RawResource, cancellationToken);
}

return await next(cancellationToken);
Expand Down
18 changes: 18 additions & 0 deletions src/Microsoft.Health.Fhir.Core/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions src/Microsoft.Health.Fhir.Core/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,12 @@
<data name="SearchParameterDefinitionDuplicatedEntry" xml:space="preserve">
<value>A search parameter with the same definition URL '{0}' already exists.</value>
</data>
<data name="SearchParameterDefinitionCannotUpdateSpecDefined" xml:space="preserve">
<value>Cannot update a spec-defined search parameter with URL '{0}'. Only custom search parameters can be updated.</value>
</data>
<data name="SearchParameterDefinitionCannotDeleteSpecDefined" xml:space="preserve">
<value>Cannot delete a spec-defined search parameter with URL '{0}'. Only custom search parameters can be deleted.</value>
</data>
<data name="SearchParameterDefinitionInvalidComponent" xml:space="preserve">
<value>SearchParameter[{0}].component is null or empty.</value>
<comment>{0} Is the Uri Identifying the SearchParameter</comment>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
using Hl7.Fhir.Serialization;
using Microsoft.Health.Fhir.Core.Exceptions;
using Microsoft.Health.Fhir.Core.Extensions;
using Microsoft.Health.Fhir.Core.Features.Definition;
using Microsoft.Health.Fhir.Core.Features.Persistence;
using Microsoft.Health.Fhir.Core.Features.Search.Parameters;
using Microsoft.Health.Fhir.Core.Features.Search.Registry;
using Microsoft.Health.Fhir.Core.Messages.Create;
using Microsoft.Health.Fhir.Core.Messages.Delete;
using Microsoft.Health.Fhir.Core.Messages.Upsert;
Expand Down Expand Up @@ -109,7 +111,8 @@ public async Task GivenADeleteResourceRequest_WhenDeletingAResourceOtherThanSear

var response = new DeleteResourceResponse(key);

var behavior = new DeleteSearchParameterBehavior<DeleteResourceRequest, DeleteResourceResponse>(_searchParameterOperations, _fhirDataStore);
var searchParameterDefinitionManager = Substitute.For<ISearchParameterDefinitionManager>();
var behavior = new DeleteSearchParameterBehavior<DeleteResourceRequest, DeleteResourceResponse>(_searchParameterOperations, _fhirDataStore, searchParameterDefinitionManager);
await behavior.Handle(request, async (ct) => await Task.Run(() => response), CancellationToken.None);

// Ensure for non-SearchParameter, that we do not call Add SearchParameter
Expand All @@ -130,7 +133,8 @@ public async Task GivenADeleteResourceRequest_WhenDeletingASearchParameterResour

var response = new DeleteResourceResponse(key);

var behavior = new DeleteSearchParameterBehavior<DeleteResourceRequest, DeleteResourceResponse>(_searchParameterOperations, _fhirDataStore);
var searchParameterDefinitionManager = Substitute.For<ISearchParameterDefinitionManager>();
var behavior = new DeleteSearchParameterBehavior<DeleteResourceRequest, DeleteResourceResponse>(_searchParameterOperations, _fhirDataStore, searchParameterDefinitionManager);
await behavior.Handle(request, async (ct) => await Task.Run(() => response), CancellationToken.None);

await _searchParameterOperations.Received().DeleteSearchParameterAsync(Arg.Any<RawResource>(), Arg.Any<CancellationToken>());
Expand All @@ -150,12 +154,56 @@ public async Task GivenADeleteResourceRequest_WhenDeletingAnAlreadyDeletedSearch

var response = new DeleteResourceResponse(key);

var behavior = new DeleteSearchParameterBehavior<DeleteResourceRequest, DeleteResourceResponse>(_searchParameterOperations, _fhirDataStore);
var searchParameterDefinitionManager = Substitute.For<ISearchParameterDefinitionManager>();
var behavior = new DeleteSearchParameterBehavior<DeleteResourceRequest, DeleteResourceResponse>(_searchParameterOperations, _fhirDataStore, searchParameterDefinitionManager);
await behavior.Handle(request, async (ct) => await Task.Run(() => response), CancellationToken.None);

await _searchParameterOperations.DidNotReceive().DeleteSearchParameterAsync(Arg.Any<RawResource>(), Arg.Any<CancellationToken>());
}

[Fact]
public async Task GivenADeleteResourceRequest_WhenDeletingASpecDefinedSearchParameterResource_ThenDeleteSearchParameterShouldNotBeCalled()
{
var searchParameter = new SearchParameter() { Id = "code", Url = "http://hl7.org/fhir/SearchParameter/code" };
var key = new ResourceKey("SearchParameter", "code");
var request = new DeleteResourceRequest(key, DeleteOperation.SoftDelete);

// Spec-defined parameters do not exist in the Resource table, so GetAsync returns null
_fhirDataStore.GetAsync(key, Arg.Any<CancellationToken>()).Returns((ResourceWrapper)null);

Check warning

Code scanning / CodeQL

Useless upcast

There is no need to upcast from [null](1) to [ResourceWrapper](2) - the conversion can be done implicitly.

Copilot Autofix

AI 4 days ago

To fix this issue, remove the explicit upcast to ResourceWrapper from the expression (ResourceWrapper)null and simply use null. This makes the code cleaner and more idiomatic. The change should be made at line 174, inside the GivenADeleteResourceRequest_WhenDeletingASpecDefinedSearchParameterResource_ThenDeleteSearchParameterShouldNotBeCalled test. No additional imports, methods, or definitions are needed for this change.

Suggested changeset 1
src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Search/SearchParameters/SearchParameterBehaviorTests.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Search/SearchParameters/SearchParameterBehaviorTests.cs b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Search/SearchParameters/SearchParameterBehaviorTests.cs
--- a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Search/SearchParameters/SearchParameterBehaviorTests.cs
+++ b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Search/SearchParameters/SearchParameterBehaviorTests.cs
@@ -171,7 +171,7 @@
             var request = new DeleteResourceRequest(key, DeleteOperation.SoftDelete);
 
             // Spec-defined parameters do not exist in the Resource table, so GetAsync returns null
-            _fhirDataStore.GetAsync(key, Arg.Any<CancellationToken>()).Returns((ResourceWrapper)null);
+            _fhirDataStore.GetAsync(key, Arg.Any<CancellationToken>()).Returns(null);
 
             var response = new DeleteResourceResponse(key);
 
EOF
@@ -171,7 +171,7 @@
var request = new DeleteResourceRequest(key, DeleteOperation.SoftDelete);

// Spec-defined parameters do not exist in the Resource table, so GetAsync returns null
_fhirDataStore.GetAsync(key, Arg.Any<CancellationToken>()).Returns((ResourceWrapper)null);
_fhirDataStore.GetAsync(key, Arg.Any<CancellationToken>()).Returns(null);

var response = new DeleteResourceResponse(key);

Copilot is powered by AI and may make mistakes. Always verify output.

var response = new DeleteResourceResponse(key);

// Setup the definition manager to recognize this as a spec-defined parameter
var searchParameterDefinitionManager = Substitute.For<ISearchParameterDefinitionManager>();
var specDefinedInfo = new SearchParameterInfo(
"code",
"code",
ValueSets.SearchParamType.Token,
new System.Uri(searchParameter.Url))
{
SearchParameterStatus = SearchParameterStatus.Supported,
};

// Mock TryGetSearchParameter to return false (it won't match the simple URL pattern)
// But AllSearchParameters will contain it and be found by the EndsWith check
searchParameterDefinitionManager.TryGetSearchParameter(Arg.Any<string>(), out Arg.Any<SearchParameterInfo>())
.Returns(x =>
{
x[1] = null;
return false;
});

searchParameterDefinitionManager.AllSearchParameters.Returns(new[] { specDefinedInfo });

var behavior = new DeleteSearchParameterBehavior<DeleteResourceRequest, DeleteResourceResponse>(_searchParameterOperations, _fhirDataStore, searchParameterDefinitionManager);

// Expect MethodNotAllowedException to be thrown for spec-defined parameters
await Assert.ThrowsAsync<MethodNotAllowedException>(() => behavior.Handle(request, async (ct) => await Task.Run(() => response), CancellationToken.None));

// Ensure DeleteSearchParameterAsync is not called for spec-defined parameters
await _searchParameterOperations.DidNotReceive().DeleteSearchParameterAsync(Arg.Any<RawResource>(), Arg.Any<CancellationToken>());
}

[Fact]
public async Task GivenAnUpsertResourceRequest_WhenSearchParameterDoesNotExist_ThenAddSearchParameterShouldBeCalled()
{
Expand Down
Loading
Loading