diff --git a/src/Microsoft.Health.Fhir.Core.UnitTests/Extensions/ConfigurationExtensionsTests.cs b/src/Microsoft.Health.Fhir.Core.UnitTests/Extensions/ConfigurationExtensionsTests.cs
new file mode 100644
index 0000000000..951fc90bf3
--- /dev/null
+++ b/src/Microsoft.Health.Fhir.Core.UnitTests/Extensions/ConfigurationExtensionsTests.cs
@@ -0,0 +1,55 @@
+// -------------------------------------------------------------------------------------------------
+// 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.Fhir.Core.Extensions;
+using Microsoft.Health.Fhir.Core.Registration;
+using Microsoft.Health.Fhir.Tests.Common;
+using Microsoft.Health.Test.Utilities;
+using Xunit;
+
+namespace Microsoft.Health.Fhir.Core.UnitTests.Extensions
+{
+ [Trait(Traits.OwningTeam, OwningTeam.Fhir)]
+ [Trait(Traits.Category, Categories.Operations)]
+ public sealed class ConfigurationExtensionsTests
+ {
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(" ")]
+ [InlineData("\t\r\n")]
+ public void GivenAnEmptyRuntimeStateConfiguration_WhenGettingRuntimeState_ThenActiveIsReturned(string configuredRuntimeState)
+ {
+ FhirRuntimeState actual = ConfigurationExtensions.GetRuntimeStateConfiguration(configuredRuntimeState);
+
+ Assert.Equal(FhirRuntimeState.Active, actual);
+ }
+
+ [Theory]
+ [InlineData("Active", FhirRuntimeState.Active)]
+ [InlineData(" active ", FhirRuntimeState.Active)]
+ [InlineData("DEPRECATED", FhirRuntimeState.Deprecated)]
+ [InlineData(" deprecated ", FhirRuntimeState.Deprecated)]
+ public void GivenAValidRuntimeStateConfiguration_WhenGettingRuntimeState_ThenConfiguredStateIsReturned(
+ string configuredRuntimeState,
+ FhirRuntimeState expected)
+ {
+ FhirRuntimeState actual = ConfigurationExtensions.GetRuntimeStateConfiguration(configuredRuntimeState);
+
+ Assert.Equal(expected, actual);
+ }
+
+ [Theory]
+ [InlineData("Unknown")]
+ [InlineData("1")]
+ [InlineData("2")]
+ public void GivenAnInvalidRuntimeStateConfiguration_WhenGettingRuntimeState_ThenActiveIsReturned(string configuredRuntimeState)
+ {
+ FhirRuntimeState actual = ConfigurationExtensions.GetRuntimeStateConfiguration(configuredRuntimeState);
+
+ Assert.Equal(FhirRuntimeState.Active, actual);
+ }
+ }
+}
diff --git a/src/Microsoft.Health.Fhir.Core.UnitTests/Registration/FhirRuntimeConfigurationTests.cs b/src/Microsoft.Health.Fhir.Core.UnitTests/Registration/FhirRuntimeConfigurationTests.cs
index 3d56bdd7da..9700965600 100644
--- a/src/Microsoft.Health.Fhir.Core.UnitTests/Registration/FhirRuntimeConfigurationTests.cs
+++ b/src/Microsoft.Health.Fhir.Core.UnitTests/Registration/FhirRuntimeConfigurationTests.cs
@@ -19,11 +19,13 @@ public sealed class FhirRuntimeConfigurationTests
public void GivenARuntimeConfiguration_WhenForAzureApiForFHIR_FollowsTheExpectedValues()
{
// Azure API For FHIR.
- IFhirRuntimeConfiguration runtimeConfiguration = new AzureApiForFhirRuntimeConfiguration();
+ IFhirRuntimeConfiguration runtimeConfiguration = new AzureApiForFhirRuntimeConfiguration(FhirRuntimeState.Deprecated);
// Support to Cosmos Db.
Assert.Equal(KnownDataStores.CosmosDb, runtimeConfiguration.DataStore);
+ Assert.Equal(FhirRuntimeState.Deprecated, runtimeConfiguration.RuntimeState);
+
// No support to Selective Search Parameter.
Assert.False(runtimeConfiguration.IsSelectiveSearchParameterSupported);
@@ -43,6 +45,8 @@ public void GivenARuntimeConfiguration_WhenForAzureHealthDataServices_FollowsThe
// Support to SQL Server.
Assert.Equal(KnownDataStores.SqlServer, runtimeConfiguration.DataStore);
+ Assert.Equal(FhirRuntimeState.Active, runtimeConfiguration.RuntimeState);
+
// Support to Selective Search Parameter.
Assert.True(runtimeConfiguration.IsSelectiveSearchParameterSupported);
diff --git a/src/Microsoft.Health.Fhir.Core/Configs/CoreFeatureConfiguration.cs b/src/Microsoft.Health.Fhir.Core/Configs/CoreFeatureConfiguration.cs
index d1bd7249fb..8c6c408aab 100644
--- a/src/Microsoft.Health.Fhir.Core/Configs/CoreFeatureConfiguration.cs
+++ b/src/Microsoft.Health.Fhir.Core/Configs/CoreFeatureConfiguration.cs
@@ -180,5 +180,10 @@ public VersioningConfiguration Versioning
/// Gets or sets a value indicating whether SMART system scope authorization is enforced for Bulk Export.
///
public bool EnableSmartExportScopeAuthorization { get; set; } = true;
+
+ ///
+ /// Gets or sets a value indicating the runtime state of the FHIR server.
+ ///
+ public string RuntimeState { get; set; }
}
}
diff --git a/src/Microsoft.Health.Fhir.Core/Extensions/ConfigurationExtensions.cs b/src/Microsoft.Health.Fhir.Core/Extensions/ConfigurationExtensions.cs
new file mode 100644
index 0000000000..45dff92674
--- /dev/null
+++ b/src/Microsoft.Health.Fhir.Core/Extensions/ConfigurationExtensions.cs
@@ -0,0 +1,31 @@
+// -------------------------------------------------------------------------------------------------
+// Copyright (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.Fhir.Core.Registration;
+
+namespace Microsoft.Health.Fhir.Core.Extensions
+{
+ public static class ConfigurationExtensions
+ {
+ public static FhirRuntimeState GetRuntimeStateConfiguration(string configuredRuntimeState)
+ {
+ if (string.IsNullOrWhiteSpace(configuredRuntimeState))
+ {
+ return FhirRuntimeState.Active;
+ }
+
+ string normalizedRuntimeState = configuredRuntimeState.Trim();
+ if (Enum.TryParse(normalizedRuntimeState, ignoreCase: true, out FhirRuntimeState runtimeState) &&
+ Enum.IsDefined(runtimeState) &&
+ string.Equals(normalizedRuntimeState, runtimeState.ToString(), StringComparison.OrdinalIgnoreCase))
+ {
+ return runtimeState;
+ }
+
+ return FhirRuntimeState.Active;
+ }
+ }
+}
diff --git a/src/Microsoft.Health.Fhir.Core/Registration/AzureApiForFhirRuntimeConfiguration.cs b/src/Microsoft.Health.Fhir.Core/Registration/AzureApiForFhirRuntimeConfiguration.cs
index 2f5156179f..e47dc37278 100644
--- a/src/Microsoft.Health.Fhir.Core/Registration/AzureApiForFhirRuntimeConfiguration.cs
+++ b/src/Microsoft.Health.Fhir.Core/Registration/AzureApiForFhirRuntimeConfiguration.cs
@@ -9,8 +9,15 @@ namespace Microsoft.Health.Fhir.Core.Registration
{
public class AzureApiForFhirRuntimeConfiguration : IFhirRuntimeConfiguration
{
+ public AzureApiForFhirRuntimeConfiguration(FhirRuntimeState runtimeState = FhirRuntimeState.Active)
+ {
+ RuntimeState = runtimeState;
+ }
+
public string DataStore => KnownDataStores.CosmosDb;
+ public FhirRuntimeState RuntimeState { get; }
+
public bool IsSelectiveSearchParameterSupported => false;
public bool IsCustomerKeyValidationBackgroundWorkerSupported => false;
diff --git a/src/Microsoft.Health.Fhir.Core/Registration/AzureHealthDataServicesRuntimeConfiguration.cs b/src/Microsoft.Health.Fhir.Core/Registration/AzureHealthDataServicesRuntimeConfiguration.cs
index 04dce8dc17..66ebb151c2 100644
--- a/src/Microsoft.Health.Fhir.Core/Registration/AzureHealthDataServicesRuntimeConfiguration.cs
+++ b/src/Microsoft.Health.Fhir.Core/Registration/AzureHealthDataServicesRuntimeConfiguration.cs
@@ -11,6 +11,8 @@ public class AzureHealthDataServicesRuntimeConfiguration : IFhirRuntimeConfigura
{
public string DataStore => KnownDataStores.SqlServer;
+ public FhirRuntimeState RuntimeState => FhirRuntimeState.Active;
+
public bool IsSelectiveSearchParameterSupported => true;
public bool IsCustomerKeyValidationBackgroundWorkerSupported => true;
diff --git a/src/Microsoft.Health.Fhir.Core/Registration/FhirRuntimeState.cs b/src/Microsoft.Health.Fhir.Core/Registration/FhirRuntimeState.cs
new file mode 100644
index 0000000000..9f177bf021
--- /dev/null
+++ b/src/Microsoft.Health.Fhir.Core/Registration/FhirRuntimeState.cs
@@ -0,0 +1,23 @@
+// -------------------------------------------------------------------------------------------------
+// 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.Fhir.Core.Registration
+{
+ ///
+ /// The effective runtime state of the FHIR service.
+ ///
+ public enum FhirRuntimeState
+ {
+ ///
+ /// The FHIR service is active.
+ ///
+ Active,
+
+ ///
+ /// The FHIR service is deprecated.
+ ///
+ Deprecated,
+ }
+}
diff --git a/src/Microsoft.Health.Fhir.Core/Registration/IFhirRuntimeConfiguration.cs b/src/Microsoft.Health.Fhir.Core/Registration/IFhirRuntimeConfiguration.cs
index 09205dc15a..1bd52f231d 100644
--- a/src/Microsoft.Health.Fhir.Core/Registration/IFhirRuntimeConfiguration.cs
+++ b/src/Microsoft.Health.Fhir.Core/Registration/IFhirRuntimeConfiguration.cs
@@ -9,6 +9,11 @@ public interface IFhirRuntimeConfiguration
{
string DataStore { get; }
+ ///
+ /// Gets the effective runtime state of the FHIR service.
+ ///
+ FhirRuntimeState RuntimeState { get; }
+
///
/// Selective Search Parameter.
///
diff --git a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Controllers/ExportControllerTests.cs b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Controllers/ExportControllerTests.cs
index 078c9d1385..3bf22246d3 100644
--- a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Controllers/ExportControllerTests.cs
+++ b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Controllers/ExportControllerTests.cs
@@ -419,7 +419,7 @@ await Assert.ThrowsAsync(() => exportController.Export
public async Task GivenASystemLevelExport_WhenRequestSentToMediator_CorrectIsParallelValueInRequest(bool isApiForFhir, bool expectedIsParallel, bool? inputIsParallelValue)
{
// Get export controller with specific runtime configuration (if needed).
- IFhirRuntimeConfiguration fhirConfig = isApiForFhir ? Substitute.For() : Substitute.For();
+ IFhirRuntimeConfiguration fhirConfig = isApiForFhir ? new AzureApiForFhirRuntimeConfiguration(runtimeState: FhirRuntimeState.Active) : new AzureHealthDataServicesRuntimeConfiguration();
var exportController = GetController(_exportEnabledJobConfiguration, _featureConfiguration, _artifactStoreConfig, fhirConfig);
// Setup additional dependencies needed for test execution.
diff --git a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Resources/Bundle/BundleHandlerRuntimeTests.cs b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Resources/Bundle/BundleHandlerOperationsTests.cs
similarity index 56%
rename from src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Resources/Bundle/BundleHandlerRuntimeTests.cs
rename to src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Resources/Bundle/BundleHandlerOperationsTests.cs
index 4a55b78757..bc09e921f3 100644
--- a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Resources/Bundle/BundleHandlerRuntimeTests.cs
+++ b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Resources/Bundle/BundleHandlerOperationsTests.cs
@@ -8,15 +8,18 @@
using System.Net;
using System.Threading;
using Hl7.Fhir.Model;
+using Hl7.Fhir.Serialization;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using Microsoft.Health.Fhir.Api.Features.Bundle;
using Microsoft.Health.Fhir.Api.Features.Resources.Bundle;
using Microsoft.Health.Fhir.Core.Configs;
+using Microsoft.Health.Fhir.Core.Exceptions;
using Microsoft.Health.Fhir.Core.Features.Persistence.Orchestration;
using Microsoft.Health.Fhir.Core.Models;
using Microsoft.Health.Fhir.Tests.Common;
using Microsoft.Health.Test.Utilities;
+using NSubstitute;
using Xunit;
using static Hl7.Fhir.Model.Bundle;
using Task = System.Threading.Tasks.Task;
@@ -25,7 +28,7 @@ namespace Microsoft.Health.Fhir.Api.UnitTests.Features.Resources.Bundle
{
[Trait(Traits.OwningTeam, OwningTeam.Fhir)]
[Trait(Traits.Category, Categories.Bundle)]
- public class BundleHandlerRuntimeTests
+ public class BundleHandlerOperationsTests
{
private readonly BundleConfiguration _bundleConfiguration = new BundleConfiguration();
@@ -40,7 +43,7 @@ public void GetDefaultBundleProcessingLogic_BatchAndTransaction_ReturnsExpectedP
var httpContext = GetHttpContext();
// Act
- var result = BundleHandlerRuntime.GetBundleProcessingLogic(_bundleConfiguration, httpContext, bundleType);
+ var result = BundleHandlerOperations.GetBundleProcessingLogic(_bundleConfiguration, httpContext, bundleType);
// Assert
Assert.Equal(expectedDefaultProcessingLogic, result);
@@ -53,7 +56,7 @@ public void GetBundleProcessingLogic_NullBundleType_ReturnsSequential()
var httpContext = GetHttpContext();
// Act
- var result = BundleHandlerRuntime.GetBundleProcessingLogic(_bundleConfiguration, httpContext, null);
+ var result = BundleHandlerOperations.GetBundleProcessingLogic(_bundleConfiguration, httpContext, null);
// Assert
Assert.Equal(BundleProcessingLogic.Sequential, result);
@@ -73,7 +76,7 @@ public void IsBundleProcessingLogicSetValid_DelegatesToHttpContext(string input)
httpContext.Request.Headers.Append(BundleOrchestratorNamingConventions.HttpHeaderBundleProcessingLogic, new StringValues(input));
// Act
- var result = BundleHandlerRuntime.IsBundleProcessingLogicValid(httpContext);
+ var result = BundleHandlerOperations.IsBundleProcessingLogicValid(httpContext);
// Assert
Assert.True(result);
@@ -91,7 +94,7 @@ public void IsBundleProcessingLogicSetValid_DelegatesToHttpContext_HandleInvalid
httpContext.Request.Headers.Append(BundleOrchestratorNamingConventions.HttpHeaderBundleProcessingLogic, new StringValues(input));
// Act
- var result = BundleHandlerRuntime.IsBundleProcessingLogicValid(httpContext);
+ var result = BundleHandlerOperations.IsBundleProcessingLogicValid(httpContext);
// Assert
Assert.False(result);
@@ -101,7 +104,7 @@ public void IsBundleProcessingLogicSetValid_DelegatesToHttpContext_HandleInvalid
public void GetBundleProcessingLogic_NullHttpContext_Throws()
{
// Act & Assert
- Assert.Throws(() => BundleHandlerRuntime.GetBundleProcessingLogic(_bundleConfiguration, null, BundleType.Batch));
+ Assert.Throws(() => BundleHandlerOperations.GetBundleProcessingLogic(_bundleConfiguration, null, BundleType.Batch));
}
[Fact]
@@ -110,7 +113,7 @@ public void IsTransactionCancelledByClient_WhenTrue()
const int timeWhenCustomerCancelledTheOperation = 4;
const int maxTransactionExecutionTimeInSeconds = 5;
- var result = BundleHandlerRuntime.HasCancellationHappenedBeforeMaxExecutionTime(
+ var result = BundleHandlerOperations.HasCancellationHappenedBeforeMaxExecutionTime(
TimeSpan.FromSeconds(timeWhenCustomerCancelledTheOperation),
new BundleConfiguration { MaxExecutionTimeInSeconds = maxTransactionExecutionTimeInSeconds },
new CancellationToken(canceled: true));
@@ -123,7 +126,7 @@ public void IsTransactionCancelledByClient_WhenTrue()
[InlineData(true, 5, 5)]
public void IsTransactionCancelledByClient_WhenFalse(bool isCancelled, int transactionElapsedTime, int maxTransactionExecutionTime)
{
- var result = BundleHandlerRuntime.HasCancellationHappenedBeforeMaxExecutionTime(
+ var result = BundleHandlerOperations.HasCancellationHappenedBeforeMaxExecutionTime(
TimeSpan.FromSeconds(transactionElapsedTime),
new BundleConfiguration { MaxExecutionTimeInSeconds = maxTransactionExecutionTime },
new CancellationToken(canceled: isCancelled));
@@ -156,7 +159,7 @@ public async Task GetPrioritizedClientException_WhenMultipleExceptionsHappened_T
}
catch (Exception)
{
- FhirTransactionFailedException ftfe = BundleHandlerRuntime.GetPrioritizedClientException(mainTask.Exception);
+ FhirTransactionFailedException ftfe = BundleHandlerOperations.GetPrioritizedClientException(mainTask.Exception);
Assert.NotNull(ftfe);
Assert.Equal(HttpStatusCode.PreconditionFailed, ftfe.ResponseStatusCode);
@@ -185,7 +188,7 @@ public async Task GetPrioritizedClientException_WhenMultipleExceptionsHappened_T
}
catch (Exception)
{
- FhirTransactionFailedException ftfe = BundleHandlerRuntime.GetPrioritizedClientException(mainTask.Exception);
+ FhirTransactionFailedException ftfe = BundleHandlerOperations.GetPrioritizedClientException(mainTask.Exception);
Assert.NotNull(ftfe);
Assert.Equal(HttpStatusCode.FailedDependency, ftfe.ResponseStatusCode);
@@ -209,7 +212,7 @@ public async Task GetPrioritizedClientException_WhenNoBaseFhirTransactionExcepti
}
catch (Exception e)
{
- FhirTransactionFailedException ftfe = BundleHandlerRuntime.GetPrioritizedClientException(mainTask.Exception);
+ FhirTransactionFailedException ftfe = BundleHandlerOperations.GetPrioritizedClientException(mainTask.Exception);
Assert.Null(ftfe);
Assert.True(e is InvalidOperationException);
@@ -237,7 +240,7 @@ public async Task GetPrioritizedClientException_WhenMultipleCancelledExceptionsH
}
catch (Exception e)
{
- FhirTransactionFailedException ftfe = BundleHandlerRuntime.GetPrioritizedClientException(mainTask.Exception);
+ FhirTransactionFailedException ftfe = BundleHandlerOperations.GetPrioritizedClientException(mainTask.Exception);
Assert.Null(ftfe);
@@ -245,6 +248,191 @@ public async Task GetPrioritizedClientException_WhenMultipleCancelledExceptionsH
}
}
+ [Fact]
+ public void ContainsSearchParams_WhenSearchParameterResourceExists_ReturnsTrue()
+ {
+ var bundle = new Hl7.Fhir.Model.Bundle
+ {
+ Entry =
+ [
+ new EntryComponent
+ {
+ Resource = new SearchParameter { Url = "http://example.org/sp-1" },
+ },
+ ],
+ };
+
+ bool result = BundleHandlerOperations.ContainsSearchParams(bundle);
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void ContainsSearchParams_WhenDeleteSearchParameterRequestExists_ReturnsTrue()
+ {
+ var bundle = new Hl7.Fhir.Model.Bundle
+ {
+ Entry =
+ [
+ new EntryComponent
+ {
+ Request = new RequestComponent
+ {
+ Method = HTTPVerb.DELETE,
+ Url = "SearchParameter/custom-sp",
+ },
+ },
+ ],
+ };
+
+ bool result = BundleHandlerOperations.ContainsSearchParams(bundle);
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void ContainsSearchParams_WhenBundleDoesNotContainSearchParameters_ReturnsFalse()
+ {
+ var bundle = new Hl7.Fhir.Model.Bundle
+ {
+ Entry =
+ [
+ new EntryComponent
+ {
+ Resource = new Patient(),
+ },
+ ],
+ };
+
+ bool result = BundleHandlerOperations.ContainsSearchParams(bundle);
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void CheckSearchParamInputAndPossibleConflicts_WhenNoSearchParamsInBundle_ReturnsFalse()
+ {
+ var bundle = new Hl7.Fhir.Model.Bundle
+ {
+ Entry =
+ [
+ new EntryComponent
+ {
+ Resource = new Patient(),
+ },
+ ],
+ };
+
+ bool result = BundleHandlerOperations.CheckSearchParamInputAndPossibleConflicts(bundle, Substitute.For());
+
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void CheckSearchParamInputAndPossibleConflicts_WhenSearchParamsHaveNoConflicts_ReturnsTrue()
+ {
+ var bundle = new Hl7.Fhir.Model.Bundle
+ {
+ Entry =
+ [
+ CreateSearchParameterEntry("code-a", "http://example.org/sp-a"),
+ CreateSearchParameterEntry("code-b", "http://example.org/sp-b"),
+ ],
+ };
+
+ bool result = BundleHandlerOperations.CheckSearchParamInputAndPossibleConflicts(bundle, Substitute.For());
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void CheckSearchParamInputAndPossibleConflicts_WhenDeleteSearchParameterRequestExists_ReturnsTrue()
+ {
+ var bundle = new Hl7.Fhir.Model.Bundle
+ {
+ Entry =
+ [
+ new EntryComponent
+ {
+ Request = new RequestComponent
+ {
+ Method = HTTPVerb.DELETE,
+ Url = "SearchParameter/obsolete-sp",
+ },
+ },
+ ],
+ };
+
+ bool result = BundleHandlerOperations.CheckSearchParamInputAndPossibleConflicts(bundle, Substitute.For());
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void CheckSearchParamInputAndPossibleConflicts_WhenDuplicateCodesExist_ThrowsRequestNotValidException()
+ {
+ var bundle = new Hl7.Fhir.Model.Bundle
+ {
+ Entry =
+ [
+ CreateSearchParameterEntry("duplicate-code", "http://example.org/sp-1"),
+ CreateSearchParameterEntry("duplicate-code", "http://example.org/sp-2"),
+ ],
+ };
+
+ var ex = Assert.Throws(() =>
+ BundleHandlerOperations.CheckSearchParamInputAndPossibleConflicts(bundle, Substitute.For()));
+
+ Assert.Contains("duplicate-code", ex.Message);
+ }
+
+ [Fact]
+ public void CheckSearchParamInputAndPossibleConflicts_WhenDuplicateUrlsExist_ThrowsRequestNotValidException()
+ {
+ var bundle = new Hl7.Fhir.Model.Bundle
+ {
+ Entry =
+ [
+ CreateSearchParameterEntry("code-1", "http://example.org/shared-url"),
+ CreateSearchParameterEntry("code-2", "http://example.org/shared-url"),
+ ],
+ };
+
+ var ex = Assert.Throws(() =>
+ BundleHandlerOperations.CheckSearchParamInputAndPossibleConflicts(bundle, Substitute.For()));
+
+ Assert.Contains("http://example.org/shared-url", ex.Message);
+ }
+
+ [Fact]
+ public void CheckSearchParamInputAndPossibleConflicts_WhenDuplicateCodesAndUrlsExist_ThrowsRequestNotValidException()
+ {
+ var bundle = new Hl7.Fhir.Model.Bundle
+ {
+ Entry =
+ [
+ CreateSearchParameterEntry("duplicate-code", "http://example.org/shared-url"),
+ CreateSearchParameterEntry("duplicate-code", "http://example.org/shared-url"),
+ ],
+ };
+
+ var ex = Assert.Throws(() =>
+ BundleHandlerOperations.CheckSearchParamInputAndPossibleConflicts(bundle, Substitute.For()));
+
+ Assert.Contains("duplicate-code", ex.Message);
+ Assert.Contains("http://example.org/shared-url", ex.Message);
+ }
+
+ private static EntryComponent CreateSearchParameterEntry(string code, string url)
+ {
+ var parser = new FhirJsonParser();
+ var searchParameter = parser.Parse($"{{\"resourceType\":\"SearchParameter\",\"url\":\"{url}\",\"code\":\"{code}\",\"base\":[\"Patient\"]}}");
+
+ return new EntryComponent
+ {
+ Resource = searchParameter,
+ };
+ }
+
private static HttpContext GetHttpContext()
{
var httpContext = new DefaultHttpContext()
diff --git a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Microsoft.Health.Fhir.Shared.Api.UnitTests.projitems b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Microsoft.Health.Fhir.Shared.Api.UnitTests.projitems
index 97d77516bc..886412be63 100644
--- a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Microsoft.Health.Fhir.Shared.Api.UnitTests.projitems
+++ b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Microsoft.Health.Fhir.Shared.Api.UnitTests.projitems
@@ -83,7 +83,7 @@
-
+
diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/Bundle/BundleHandler.cs b/src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/Bundle/BundleHandler.cs
index 6555c41b35..79fe2ca7eb 100644
--- a/src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/Bundle/BundleHandler.cs
+++ b/src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/Bundle/BundleHandler.cs
@@ -182,7 +182,7 @@ public BundleHandler(
// Set optimized-query processing logic.
_optimizedQuerySet = SetRequestContextWithOptimizedQuerying(_outerHttpContext, fhirRequestContextAccessor.RequestContext, _logger);
- _isBundleProcessingLogicValid = _bundleOrchestrator.IsEnabled ? BundleHandlerRuntime.IsBundleProcessingLogicValid(_outerHttpContext) : true;
+ _isBundleProcessingLogicValid = _bundleOrchestrator.IsEnabled ? BundleHandlerOperations.IsBundleProcessingLogicValid(_outerHttpContext) : true;
}
public async Task HandleAsync(BundleRequest request, CancellationToken cancellationToken)
@@ -213,7 +213,7 @@ public async Task HandleAsync(BundleRequest request, Cancellatio
_bundleType = bundleResource.Type;
// Retrieve bundle processing logic.
- BundleProcessingLogic bundleProcessingLogic = _bundleOrchestrator.IsEnabled ? BundleHandlerRuntime.GetBundleProcessingLogic(_bundleConfiguration, _outerHttpContext, _bundleType) : BundleProcessingLogic.Sequential;
+ BundleProcessingLogic bundleProcessingLogic = _bundleOrchestrator.IsEnabled ? BundleHandlerOperations.GetBundleProcessingLogic(_bundleConfiguration, _outerHttpContext, _bundleType) : BundleProcessingLogic.Sequential;
if (_bundleType == BundleType.Batch)
{
@@ -251,9 +251,7 @@ public async Task HandleAsync(BundleRequest request, Cancellatio
_logger.LogInformation("Edge Case scenario: sequential transactional bundle has a single record, and it's now changed to execute as parallel.");
bundleProcessingLogic = BundleProcessingLogic.Parallel;
}
- else if (bundleResource.Entry.Any(e => e.Resource?.TypeName == KnownResourceTypes.SearchParameter
- //// for deletes type name is not populated, so checking url
- || e.Request?.Url?.StartsWith(KnownResourceTypes.SearchParameter, StringComparison.OrdinalIgnoreCase) == true))
+ else if (BundleHandlerOperations.ContainsSearchParams(bundleResource))
{
// SearchParameter persistence relies on the parallel-bundle path (MergeResourcesAndSearchParams)
// for atomic resource + status row commit, so any sequential transaction bundle containing a
@@ -290,49 +288,7 @@ public async Task HandleAsync(BundleRequest request, Cancellatio
private async Task CheckSearchParamInputConflictsAndUpdateCache(Hl7.Fhir.Model.Bundle bundle, CancellationToken cancellationToken)
{
- var codes = new HashSet<(string Type, string Code)>();
- var urls = new HashSet();
- var dupCodes = new HashSet<(string Type, string Code)>();
- var dupUrls = new HashSet();
- var searchParamsInBundle = false;
- foreach (var param in bundle.Entry.Select(_ => _.Resource as SearchParameter).Where(_ => _ != null))
- {
- if (param.Code != null && param.Base != null)
- {
- var allResourceTypes = SearchParameterDefinitionManager.GetDerivedResourceTypes(_modelInfoProvider, param.Base.Where(_ => _ != null).Select(_ => _.Value.ToString()).ToList());
- foreach (var resourceType in allResourceTypes.Where(_ => !codes.Add((_, param.Code))))
- {
- dupCodes.Add((resourceType, param.Code));
- }
- }
-
- if (param.Url != null && !urls.Add(param.Url))
- {
- dupUrls.Add(param.Url);
- }
-
- searchParamsInBundle = true;
- }
-
- if (dupCodes.Count > 0 || dupUrls.Count > 0)
- {
- if (dupCodes.Count == 0)
- {
- throw new RequestNotValidException(string.Format(Api.Resources.DuplicateSearchParamUrlsInBundle, string.Join(", ", dupUrls)));
- }
- else if (dupUrls.Count == 0)
- {
- throw new RequestNotValidException(string.Format(Api.Resources.DuplicateSearchParamCodesInBundle, string.Join(", ", dupCodes)));
- }
-
- throw new RequestNotValidException(string.Format(Api.Resources.DuplicateSearchParamCodesAndUrlsInBundle, string.Join(", ", dupCodes), string.Join(", ", dupUrls)));
- }
-
- // for deletes Entry.Resource is null. need to check in other way
- if (!searchParamsInBundle && bundle.Entry.Any(e => e.Request.Method == HTTPVerb.DELETE && e.Request.Url.StartsWith(KnownResourceTypes.SearchParameter, StringComparison.OrdinalIgnoreCase)))
- {
- searchParamsInBundle = true;
- }
+ bool searchParamsInBundle = BundleHandlerOperations.CheckSearchParamInputAndPossibleConflicts(bundle, _modelInfoProvider);
if (searchParamsInBundle)
{
@@ -763,7 +719,7 @@ private async Task ExecuteRequestsWithSingleHttpVerbInSequenceAs
try
{
- await BundleHandlerRuntime.DelayWithRetryAfterAsync(httpContext, cancellationToken);
+ await BundleHandlerOperations.DelayWithRetryAfterAsync(httpContext, cancellationToken);
}
catch (OperationCanceledException oce)
{
@@ -835,7 +791,7 @@ private async Task ExecuteRequestsWithSingleHttpVerbInSequenceAs
if (_bundleType.Equals(BundleType.Transaction) && entryComponent.Response.Outcome != null)
{
- RaiseFhirTransactionException(resourceContext, httpStatusCode, entryComponent, isOperationCancelledByClient: BundleHandlerRuntime.HasCancellationHappenedBeforeMaxExecutionTime(watch.Elapsed, _bundleConfiguration, cancellationToken));
+ RaiseFhirTransactionException(resourceContext, httpStatusCode, entryComponent, isOperationCancelledByClient: BundleHandlerOperations.HasCancellationHappenedBeforeMaxExecutionTime(watch.Elapsed, _bundleConfiguration, cancellationToken));
}
responseBundle.Entry[resourceContext.Index] = entryComponent;
diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/Bundle/BundleHandlerRuntime.cs b/src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/Bundle/BundleHandlerOperations.cs
similarity index 64%
rename from src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/Bundle/BundleHandlerRuntime.cs
rename to src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/Bundle/BundleHandlerOperations.cs
index 3f499e23ae..3e1e236b0b 100644
--- a/src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/Bundle/BundleHandlerRuntime.cs
+++ b/src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/Bundle/BundleHandlerOperations.cs
@@ -9,12 +9,16 @@
using System.Net;
using System.Threading;
using EnsureThat;
+using Hl7.Fhir.Model;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using Microsoft.Health.Fhir.Api.Features.Bundle;
using Microsoft.Health.Fhir.Api.Features.Headers;
using Microsoft.Health.Fhir.Core.Configs;
+using Microsoft.Health.Fhir.Core.Exceptions;
+using Microsoft.Health.Fhir.Core.Features.Definition;
using Microsoft.Health.Fhir.Core.Models;
+using static Hl7.Fhir.ElementModel.ScopedNode;
using static Hl7.Fhir.Model.Bundle;
using Task = System.Threading.Tasks.Task;
@@ -23,7 +27,7 @@ namespace Microsoft.Health.Fhir.Api.Features.Resources.Bundle
///
/// Set of static methods used as part of the bundle handling logic.
///
- public static class BundleHandlerRuntime
+ public static class BundleHandlerOperations
{
///
/// Delay logic used in case of retry operations.
@@ -112,6 +116,68 @@ public static BundleProcessingLogic GetBundleProcessingLogic(BundleConfiguration
return BundleProcessingLogic.Sequential;
}
+ public static bool ContainsSearchParams(Hl7.Fhir.Model.Bundle bundle)
+ {
+ EnsureArg.IsNotNull(bundle, nameof(bundle));
+
+ return bundle.Entry.Any(e => e.Resource?.TypeName == KnownResourceTypes.SearchParameter ||
+ e.Request?.Url?.StartsWith(KnownResourceTypes.SearchParameter, StringComparison.OrdinalIgnoreCase) == true); // for deletes type name is not populated, so checking url
+ }
+
+ ///
+ /// Checks for duplicate search parameter codes and urls in the bundle and throws a if any duplicates are found.
+ ///
+ /// Returns true if there are search parameters in the bundle; otherwise, false.
+ /// If any duplicate search parameter codes or urls are found, a is thrown.
+ public static bool CheckSearchParamInputAndPossibleConflicts(Hl7.Fhir.Model.Bundle bundle, IModelInfoProvider modelInfoProvider)
+ {
+ var codes = new HashSet<(string Type, string Code)>();
+ var urls = new HashSet();
+ var dupCodes = new HashSet<(string Type, string Code)>();
+ var dupUrls = new HashSet();
+ var searchParamsInBundle = false;
+ foreach (var param in bundle.Entry.Select(_ => _.Resource as SearchParameter).Where(_ => _ != null))
+ {
+ if (param.Code != null && param.Base != null)
+ {
+ var allResourceTypes = SearchParameterDefinitionManager.GetDerivedResourceTypes(modelInfoProvider, param.Base.Where(_ => _ != null).Select(_ => _.Value.ToString()).ToList());
+ foreach (var resourceType in allResourceTypes.Where(_ => !codes.Add((_, param.Code))))
+ {
+ dupCodes.Add((resourceType, param.Code));
+ }
+ }
+
+ if (param.Url != null && !urls.Add(param.Url))
+ {
+ dupUrls.Add(param.Url);
+ }
+
+ searchParamsInBundle = true;
+ }
+
+ if (dupCodes.Count > 0 || dupUrls.Count > 0)
+ {
+ if (dupCodes.Count == 0)
+ {
+ throw new RequestNotValidException(string.Format(Api.Resources.DuplicateSearchParamUrlsInBundle, string.Join(", ", dupUrls)));
+ }
+ else if (dupUrls.Count == 0)
+ {
+ throw new RequestNotValidException(string.Format(Api.Resources.DuplicateSearchParamCodesInBundle, string.Join(", ", dupCodes)));
+ }
+
+ throw new RequestNotValidException(string.Format(Api.Resources.DuplicateSearchParamCodesAndUrlsInBundle, string.Join(", ", dupCodes), string.Join(", ", dupUrls)));
+ }
+
+ // for deletes Entry.Resource is null. need to check in other way
+ if (!searchParamsInBundle && bundle.Entry.Any(e => e.Request?.Method == HTTPVerb.DELETE && e.Request.Url.StartsWith(KnownResourceTypes.SearchParameter, StringComparison.OrdinalIgnoreCase)))
+ {
+ searchParamsInBundle = true;
+ }
+
+ return searchParamsInBundle;
+ }
+
///
/// Determines whether a bundle has been cancelled by the client.
/// If the cancellation is requested and the elapsed time is less than the max bundle execution time, it is assumed that the client cancelled the request.
diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/Bundle/BundleHandlerParallelOperations.cs b/src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/Bundle/BundleHandlerParallelOperations.cs
index f0b6d65f28..c9cdbe1ee6 100644
--- a/src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/Bundle/BundleHandlerParallelOperations.cs
+++ b/src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/Bundle/BundleHandlerParallelOperations.cs
@@ -5,7 +5,6 @@
using System;
using System.Collections.Generic;
-using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Net;
@@ -18,7 +17,6 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Primitives;
using Microsoft.Health.Api.Features.Audit;
using Microsoft.Health.Core.Features.Context;
using Microsoft.Health.Fhir.Api.Features.Bundle;
@@ -205,7 +203,7 @@ private async Task ExecuteRequestsInParallelAsync(
if (bundleExecutionContext.IsTransactionFailedByClientError && parallelRequests != null && parallelRequests.Exception != null)
{
// FhirTransactionFailedException - It means that the transaction failed due a possible client error.
- FhirTransactionFailedException clientException = BundleHandlerRuntime.GetPrioritizedClientException(parallelRequests.Exception);
+ FhirTransactionFailedException clientException = BundleHandlerOperations.GetPrioritizedClientException(parallelRequests.Exception);
if (clientException != null)
{
ExceptionDispatchInfo.Capture(clientException).Throw();
@@ -374,7 +372,7 @@ private static async Task HandleRequestAsync(
try
{
- await BundleHandlerRuntime.DelayWithRetryAfterAsync(httpContext, cancellationToken);
+ await BundleHandlerOperations.DelayWithRetryAfterAsync(httpContext, cancellationToken);
}
catch (OperationCanceledException oce)
{
@@ -451,7 +449,7 @@ private static async Task HandleRequestAsync(
resourceExecutionContext,
httpStatusCode,
entryComponent,
- isOperationCancelledByClient: !bundleExecutionContext.IsTransactionFailedByClientError && BundleHandlerRuntime.HasCancellationHappenedBeforeMaxExecutionTime(watch.Elapsed, bundleExecutionContext.Configuration, cancellationToken));
+ isOperationCancelledByClient: !bundleExecutionContext.IsTransactionFailedByClientError && BundleHandlerOperations.HasCancellationHappenedBeforeMaxExecutionTime(watch.Elapsed, bundleExecutionContext.Configuration, cancellationToken));
}
responseBundle.Entry[resourceExecutionContext.Index] = entryComponent;
@@ -468,7 +466,7 @@ private static EntryComponent HandleCancelledRetryRequest(Hl7.Fhir.Model.Bundle
if (bundleType.Equals(BundleType.Transaction))
{
- RaiseFhirTransactionException(resourceExecutionContext, cancelledRequestHttpStatusCode, entryComponent, isOperationCancelledByClient: !isOperationCancelledByClientError && BundleHandlerRuntime.HasCancellationHappenedBeforeMaxExecutionTime(watch.Elapsed, bundleConfiguration, cancellationToken));
+ RaiseFhirTransactionException(resourceExecutionContext, cancelledRequestHttpStatusCode, entryComponent, isOperationCancelledByClient: !isOperationCancelledByClientError && BundleHandlerOperations.HasCancellationHappenedBeforeMaxExecutionTime(watch.Elapsed, bundleConfiguration, cancellationToken));
}
// Default path for batch bundles.
diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Microsoft.Health.Fhir.Shared.Api.projitems b/src/Microsoft.Health.Fhir.Shared.Api/Microsoft.Health.Fhir.Shared.Api.projitems
index 5ec9859b13..c117ed5860 100644
--- a/src/Microsoft.Health.Fhir.Shared.Api/Microsoft.Health.Fhir.Shared.Api.projitems
+++ b/src/Microsoft.Health.Fhir.Shared.Api/Microsoft.Health.Fhir.Shared.Api.projitems
@@ -44,7 +44,7 @@
-
+
diff --git a/src/Microsoft.Health.Fhir.Shared.Web.UnitTests/StartupTests.cs b/src/Microsoft.Health.Fhir.Shared.Web.UnitTests/StartupTests.cs
index 3b67c0471f..c6dd3869f1 100644
--- a/src/Microsoft.Health.Fhir.Shared.Web.UnitTests/StartupTests.cs
+++ b/src/Microsoft.Health.Fhir.Shared.Web.UnitTests/StartupTests.cs
@@ -13,6 +13,8 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.ApplicationInsights;
using Microsoft.Extensions.Options;
+using Microsoft.Health.Fhir.Core.Features;
+using Microsoft.Health.Fhir.Core.Registration;
using Microsoft.Health.Fhir.Tests.Common;
using Microsoft.Health.Fhir.Web;
using Microsoft.Health.Test.Utilities;
@@ -38,6 +40,84 @@ public class StartupTests
private const string TelemetryProviderOpenTelemetryConfigurationValue = "OpenTelemetry";
private const string TelemetryProviderNoneConfigurationValue = "None";
private const string AddTelemetryProviderMethodName = "AddTelemetryProvider";
+ private const string AddRuntimeConfigurationMethodName = "AddRuntimeConfiguration";
+ private const string RuntimeStateConfigurationKey = "FhirServer:CoreFeatures:RuntimeState";
+
+ [Theory]
+ [InlineData(null, FhirRuntimeState.Active)]
+ [InlineData("", FhirRuntimeState.Active)]
+ [InlineData("Active", FhirRuntimeState.Active)]
+ [InlineData("Deprecated", FhirRuntimeState.Deprecated)]
+ public void GivenGen1RuntimeState_WhenAddingRuntimeConfiguration_ThenEffectiveStateIsRegistered(
+ string configuredRuntimeState,
+ FhirRuntimeState expectedRuntimeState)
+ {
+ // This test ensures that the default values are properly handled for Gen1 runtime state.
+ // Empty/null values should be treated as "Active" for Gen1 runtime state.
+ // Active/Deprecates values should be treated as-is for Gen1 runtime state.
+
+ var settings = new Dictionary
+ {
+ { "DataStore", KnownDataStores.CosmosDb },
+ { RuntimeStateConfigurationKey, configuredRuntimeState },
+ };
+ IConfiguration configuration = new ConfigurationBuilder().AddInMemoryCollection(settings).Build();
+ var services = new ServiceCollection();
+ var fhirServerBuilder = Substitute.For();
+ fhirServerBuilder.Services.Returns(services);
+
+ InvokeAddRuntimeConfiguration(configuration, fhirServerBuilder);
+
+ using ServiceProvider serviceProvider = services.BuildServiceProvider();
+ IFhirRuntimeConfiguration runtimeConfiguration = serviceProvider.GetRequiredService();
+ Assert.Equal(expectedRuntimeState, runtimeConfiguration.RuntimeState);
+ }
+
+ [Fact]
+ public void GivenDeprecatedGen2RuntimeState_WhenAddingRuntimeConfiguration_ThenEffectiveStateIsActive()
+ {
+ // This test ensures that the 'Active' is always handled for Gen2 runtime state, no matter what the configured value is.
+
+ var settings = new Dictionary
+ {
+ { "DataStore", KnownDataStores.SqlServer },
+ { RuntimeStateConfigurationKey, FhirRuntimeState.Deprecated.ToString() },
+ };
+ IConfiguration configuration = new ConfigurationBuilder().AddInMemoryCollection(settings).Build();
+ var services = new ServiceCollection();
+ var fhirServerBuilder = Substitute.For();
+ fhirServerBuilder.Services.Returns(services);
+
+ InvokeAddRuntimeConfiguration(configuration, fhirServerBuilder);
+
+ using ServiceProvider serviceProvider = services.BuildServiceProvider();
+ IFhirRuntimeConfiguration runtimeConfiguration = serviceProvider.GetRequiredService();
+ Assert.Equal(FhirRuntimeState.Active, runtimeConfiguration.RuntimeState);
+ }
+
+ [Theory]
+ [InlineData("Invalid")]
+ [InlineData("1")]
+ public void GivenInvalidGen1RuntimeState_WhenAddingRuntimeConfiguration_ThenInitializationContinuesAsActive(string configuredRuntimeState)
+ {
+ // This test ensures that invalid values are properly handled for Gen1 runtime state as 'Active'.
+
+ var settings = new Dictionary
+ {
+ { "DataStore", KnownDataStores.CosmosDb },
+ { RuntimeStateConfigurationKey, configuredRuntimeState },
+ };
+ IConfiguration configuration = new ConfigurationBuilder().AddInMemoryCollection(settings).Build();
+ var services = new ServiceCollection();
+ var fhirServerBuilder = Substitute.For();
+ fhirServerBuilder.Services.Returns(services);
+
+ InvokeAddRuntimeConfiguration(configuration, fhirServerBuilder);
+
+ using ServiceProvider serviceProvider = services.BuildServiceProvider();
+ IFhirRuntimeConfiguration runtimeConfiguration = serviceProvider.GetRequiredService();
+ Assert.Equal(FhirRuntimeState.Active, runtimeConfiguration.RuntimeState);
+ }
[Fact]
public void GivenAppSettings_WhenTelemetrySectionIsAbsent_ThenTelemetryProviderShouldBeDisabled()
@@ -178,5 +258,16 @@ private IConfiguration BuildConfiguration(string provider, string instrumentatio
.Build();
return configuration;
}
+
+ private static void InvokeAddRuntimeConfiguration(
+ IConfiguration configuration,
+ IFhirServerBuilder fhirServerBuilder)
+ {
+ MethodInfo addRuntimeConfigurationMethod = typeof(Startup).GetMethod(
+ AddRuntimeConfigurationMethodName,
+ BindingFlags.NonPublic | BindingFlags.Static);
+
+ addRuntimeConfigurationMethod.Invoke(null, new object[] { configuration, fhirServerBuilder });
+ }
}
}
diff --git a/src/Microsoft.Health.Fhir.Shared.Web/Startup.cs b/src/Microsoft.Health.Fhir.Shared.Web/Startup.cs
index edf1e62483..10615b7c78 100644
--- a/src/Microsoft.Health.Fhir.Shared.Web/Startup.cs
+++ b/src/Microsoft.Health.Fhir.Shared.Web/Startup.cs
@@ -31,7 +31,6 @@
using Microsoft.Health.Fhir.Core.Features.Telemetry;
using Microsoft.Health.Fhir.Core.Logging.Metrics;
using Microsoft.Health.Fhir.Core.Messages.Search;
-using Microsoft.Health.Fhir.Core.Messages.Storage;
using Microsoft.Health.Fhir.Core.Registration;
using Microsoft.Health.Fhir.Shared.Web;
using Microsoft.Health.Fhir.SqlServer.Features.Storage;
@@ -78,7 +77,6 @@ public virtual void ConfigureServices(IServiceCollection services)
.AddAzureIntegrationDataStoreClient(Configuration)
.AddConvertData()
.AddMemberMatch();
-
services.AddDevelopmentIdentityProvider(Configuration);
// Set the runtime configuration for the up and running service.
@@ -119,14 +117,14 @@ private void AddDataStore(IServiceCollection services, IFhirServerBuilder fhirSe
}
}
- private IFhirRuntimeConfiguration AddRuntimeConfiguration(IConfiguration configuration, IFhirServerBuilder fhirServerBuilder)
+ private static IFhirRuntimeConfiguration AddRuntimeConfiguration(IConfiguration configuration, IFhirServerBuilder fhirServerBuilder)
{
IFhirRuntimeConfiguration runtimeConfiguration = null;
- string dataStore = Configuration["DataStore"];
+ string dataStore = configuration["DataStore"];
if (KnownDataStores.IsCosmosDbDataStore(dataStore))
{
- runtimeConfiguration = new AzureApiForFhirRuntimeConfiguration();
+ runtimeConfiguration = new AzureApiForFhirRuntimeConfiguration(GetRuntimeState(configuration));
}
else if (KnownDataStores.IsSqlServerDataStore(dataStore))
{
@@ -142,6 +140,13 @@ private IFhirRuntimeConfiguration AddRuntimeConfiguration(IConfiguration configu
return runtimeConfiguration;
}
+ private static FhirRuntimeState GetRuntimeState(IConfiguration configuration)
+ {
+ string configuredRuntimeState = configuration["FhirServer:CoreFeatures:RuntimeState"];
+
+ return Core.Extensions.ConfigurationExtensions.GetRuntimeStateConfiguration(configuredRuntimeState);
+ }
+
private void AddTaskHostingService(IServiceCollection services)
{
services.Add()
@@ -175,6 +180,10 @@ private void AddTaskHostingService(IServiceCollection services)
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public virtual void Configure(IApplicationBuilder app)
{
+ IFhirRuntimeConfiguration runtimeConfiguration = app.ApplicationServices.GetRequiredService();
+ ILogger logger = app.ApplicationServices.GetRequiredService>();
+ logger.LogInformation("The effective FHIR runtime state is {RuntimeState}.", runtimeConfiguration.RuntimeState);
+
app.Use(async (context, next) =>
{
if (instanceId != null)
diff --git a/src/Microsoft.Health.Fhir.Shared.Web/appsettings.json b/src/Microsoft.Health.Fhir.Shared.Web/appsettings.json
index 8ef4b04cc4..cf11c9886f 100644
--- a/src/Microsoft.Health.Fhir.Shared.Web/appsettings.json
+++ b/src/Microsoft.Health.Fhir.Shared.Web/appsettings.json
@@ -22,6 +22,7 @@
"SupportsAnonymizedExport": true
},
"CoreFeatures": {
+ "RuntimeState": "Active",
"FhirSdkProvider": "Firely",
"SupportsBatch": true,
"SupportsTransaction": true,