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,