diff --git a/src/Microsoft.Health.Fhir.Core/Features/KnownHeaders.cs b/src/Microsoft.Health.Fhir.Core/Features/KnownHeaders.cs
index d73ca2ae25..e289a6012a 100644
--- a/src/Microsoft.Health.Fhir.Core/Features/KnownHeaders.cs
+++ b/src/Microsoft.Health.Fhir.Core/Features/KnownHeaders.cs
@@ -30,6 +30,7 @@ public static class KnownHeaders
public const string ProfileValidation = "x-ms-profile-validation";
public const string CustomAuditHeaderPrefix = "X-MS-AZUREFHIR-AUDIT-";
public const string FhirUserHeader = "x-ms-fhiruser";
+ public const string QueryLatencyOverEfficiency = "x-ms-query-latency-over-efficiency";
// #conditionalQueryParallelism - Header used to activate parallel conditional-query processing.
public const string ConditionalQueryProcessingLogic = "x-conditionalquery-processing-logic";
diff --git a/src/Microsoft.Health.Fhir.Core/Registration/AzureApiForFhirRuntimeConfiguration.cs b/src/Microsoft.Health.Fhir.Core/Registration/AzureApiForFhirRuntimeConfiguration.cs
index ae46127ff4..51b29f5b9e 100644
--- a/src/Microsoft.Health.Fhir.Core/Registration/AzureApiForFhirRuntimeConfiguration.cs
+++ b/src/Microsoft.Health.Fhir.Core/Registration/AzureApiForFhirRuntimeConfiguration.cs
@@ -18,5 +18,7 @@ public class AzureApiForFhirRuntimeConfiguration : IFhirRuntimeConfiguration
public bool IsCustomerKeyValidationBackgroundWorkerSupported => false;
public bool IsTransactionSupported => false;
+
+ public bool IsLatencyOverEfficiencySupported => true;
}
}
diff --git a/src/Microsoft.Health.Fhir.Core/Registration/AzureHealthDataServicesRuntimeConfiguration.cs b/src/Microsoft.Health.Fhir.Core/Registration/AzureHealthDataServicesRuntimeConfiguration.cs
index e59997b6d1..26ff55233a 100644
--- a/src/Microsoft.Health.Fhir.Core/Registration/AzureHealthDataServicesRuntimeConfiguration.cs
+++ b/src/Microsoft.Health.Fhir.Core/Registration/AzureHealthDataServicesRuntimeConfiguration.cs
@@ -18,5 +18,7 @@ public class AzureHealthDataServicesRuntimeConfiguration : IFhirRuntimeConfigura
public bool IsCustomerKeyValidationBackgroundWorkerSupported => true;
public bool IsTransactionSupported => true;
+
+ public bool IsLatencyOverEfficiencySupported => false;
}
}
diff --git a/src/Microsoft.Health.Fhir.Core/Registration/IFhirRuntimeConfiguration.cs b/src/Microsoft.Health.Fhir.Core/Registration/IFhirRuntimeConfiguration.cs
index 30f6915637..94411f0c67 100644
--- a/src/Microsoft.Health.Fhir.Core/Registration/IFhirRuntimeConfiguration.cs
+++ b/src/Microsoft.Health.Fhir.Core/Registration/IFhirRuntimeConfiguration.cs
@@ -28,5 +28,10 @@ public interface IFhirRuntimeConfiguration
/// Support to transactions.
///
bool IsTransactionSupported { get; }
+
+ ///
+ /// Supports the 'latency-over-efficiency' HTTP header.
+ ///
+ bool IsLatencyOverEfficiencySupported { get; }
}
}
diff --git a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Controllers/FhirControllerTests.cs b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Controllers/FhirControllerTests.cs
new file mode 100644
index 0000000000..97098237ce
--- /dev/null
+++ b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Controllers/FhirControllerTests.cs
@@ -0,0 +1,49 @@
+// -------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
+// -------------------------------------------------------------------------------------------------
+
+using System;
+using System.Linq;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Health.Api.Features.Audit;
+using Microsoft.Health.Fhir.Api.Controllers;
+using Microsoft.Health.Fhir.Api.Features.Filters;
+using Microsoft.Health.Fhir.Tests.Common;
+using Microsoft.Health.Test.Utilities;
+using Xunit;
+
+namespace Microsoft.Health.Fhir.Api.UnitTests.Controllers
+{
+ [Trait(Traits.OwningTeam, OwningTeam.Fhir)]
+ [Trait(Traits.Category, Categories.Web)]
+ public sealed class FhirControllerTests
+ {
+ [Fact]
+ public void WhenProviderAFhirController_CheckIfAllExpectedServiceFilterAttributesArePresent()
+ {
+ Type[] expectedCustomAttributes = new Type[]
+ {
+ typeof(AuditLoggingFilterAttribute),
+ typeof(OperationOutcomeExceptionFilterAttribute),
+ typeof(ValidateFormatParametersAttribute),
+ typeof(QueryLatencyOverEfficiencyFilterAttribute),
+ };
+
+ ServiceFilterAttribute[] serviceFilterAttributes = Attribute.GetCustomAttributes(typeof(FhirController), typeof(ServiceFilterAttribute))
+ .Select(a => a as ServiceFilterAttribute)
+ .ToArray();
+
+ foreach (Type expectedCustomAttribute in expectedCustomAttributes)
+ {
+ bool attributeWasFound = serviceFilterAttributes.Any(s => s.ServiceType == expectedCustomAttribute);
+
+ if (!attributeWasFound)
+ {
+ string errorMessage = $"The custom attribute '{expectedCustomAttribute.ToString()}' is not assigned to '{nameof(FhirController)}'.";
+ Assert.Fail(errorMessage);
+ }
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Filters/QueryLatencyOverEfficiencyFilterAttributeTests.cs b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Filters/QueryLatencyOverEfficiencyFilterAttributeTests.cs
new file mode 100644
index 0000000000..1af7df7ce1
--- /dev/null
+++ b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Filters/QueryLatencyOverEfficiencyFilterAttributeTests.cs
@@ -0,0 +1,100 @@
+// -------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
+// -------------------------------------------------------------------------------------------------
+
+using System.Collections.Generic;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Abstractions;
+using Microsoft.AspNetCore.Mvc.Filters;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Health.Core.Features.Context;
+using Microsoft.Health.Fhir.Api.Features.Filters;
+using Microsoft.Health.Fhir.Core.Features;
+using Microsoft.Health.Fhir.Core.Features.Context;
+using Microsoft.Health.Fhir.Core.Registration;
+using Microsoft.Health.Fhir.Core.UnitTests.Features.Context;
+using Microsoft.Health.Fhir.Tests.Common;
+using Microsoft.Health.Test.Utilities;
+using NSubstitute;
+using Xunit;
+
+namespace Microsoft.Health.Fhir.Api.UnitTests.Features.Filters
+{
+ [Trait(Traits.OwningTeam, OwningTeam.Fhir)]
+ [Trait(Traits.Category, Categories.Web)]
+ public sealed class QueryLatencyOverEfficiencyFilterAttributeTests
+ {
+ private readonly IFhirRuntimeConfiguration _azureApiForFhirConfiguration = new AzureApiForFhirRuntimeConfiguration();
+ private readonly IFhirRuntimeConfiguration _azureHealthDataServicesFhirConfiguration = new AzureHealthDataServicesRuntimeConfiguration();
+
+ [Fact]
+ public void GivenAValidHttpContextForAzureApiForFhir_WhenItContainsALatencyOverEfficiencyFlag_ThenFhirContextIsDecorated()
+ {
+ var httpRequest = GetFakeHttpContext(isLatencyOverEfficiencyEnabled: true);
+
+ var filter = new QueryLatencyOverEfficiencyFilterAttribute(httpRequest.RequestContext, _azureApiForFhirConfiguration);
+ filter.OnActionExecuting(httpRequest.ActionContext);
+
+ var fhirContextPropertyBag = httpRequest.RequestContext.RequestContext.Properties;
+
+ Assert.True(fhirContextPropertyBag.ContainsKey(KnownQueryParameterNames.OptimizeConcurrency));
+ Assert.Equal(true, fhirContextPropertyBag[KnownQueryParameterNames.OptimizeConcurrency]);
+ }
+
+ [Fact]
+ public void GivenAValidHttpContextForAzureHealthDataService_WhenItContainsALatencyOverEfficiencyFlag_ThenFhirContextIsNotDecorated()
+ {
+ // The latency-over-efficiency flag is only applicable to Azure API for FHIR.
+
+ var httpRequest = GetFakeHttpContext(isLatencyOverEfficiencyEnabled: true);
+
+ var filter = new QueryLatencyOverEfficiencyFilterAttribute(httpRequest.RequestContext, _azureHealthDataServicesFhirConfiguration);
+ filter.OnActionExecuting(httpRequest.ActionContext);
+
+ var fhirContextPropertyBag = httpRequest.RequestContext.RequestContext.Properties;
+
+ Assert.False(fhirContextPropertyBag.ContainsKey(KnownQueryParameterNames.OptimizeConcurrency));
+ }
+
+ [Fact]
+ public void GivenAValidHttpContext_WhenItDoesNotContainALatencyOverEfficiencyFlag_ThenFhirContextIsClean()
+ {
+ var httpRequest = GetFakeHttpContext(isLatencyOverEfficiencyEnabled: false);
+
+ var filter = new QueryLatencyOverEfficiencyFilterAttribute(httpRequest.RequestContext, _azureApiForFhirConfiguration);
+ filter.OnActionExecuting(httpRequest.ActionContext);
+
+ var fhirContextPropertyBag = httpRequest.RequestContext.RequestContext.Properties;
+
+ Assert.False(fhirContextPropertyBag.ContainsKey(KnownQueryParameterNames.OptimizeConcurrency));
+ }
+
+ private static (RequestContextAccessor RequestContext, ActionExecutingContext ActionContext) GetFakeHttpContext(bool isLatencyOverEfficiencyEnabled)
+ {
+ var httpContext = new DefaultHttpContext();
+
+ if (isLatencyOverEfficiencyEnabled)
+ {
+ httpContext.Request.Headers[KnownHeaders.QueryLatencyOverEfficiency] = "true";
+ }
+
+ ActionExecutingContext context = new ActionExecutingContext(
+ new ActionContext(
+ httpContext,
+ new RouteData(),
+ new ActionDescriptor()),
+ new List(),
+ actionArguments: new Dictionary(),
+ FilterTestsHelper.CreateMockFhirController());
+
+ DefaultFhirRequestContext fhirRequestContext = new DefaultFhirRequestContext();
+
+ var fhirRequestContextAccessor = Substitute.For>();
+ fhirRequestContextAccessor.RequestContext.Returns(fhirRequestContext);
+
+ return (fhirRequestContextAccessor, context);
+ }
+ }
+}
diff --git a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Headers/HttpContextExtensionsTests.cs b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Headers/HttpContextExtensionsTests.cs
new file mode 100644
index 0000000000..365b85b730
--- /dev/null
+++ b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Headers/HttpContextExtensionsTests.cs
@@ -0,0 +1,153 @@
+// -------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
+// -------------------------------------------------------------------------------------------------
+
+using System;
+using System.Collections.Generic;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Health.Fhir.Api.Features.Headers;
+using Microsoft.Health.Fhir.Api.Features.Resources;
+using Microsoft.Health.Fhir.Api.Features.Resources.Bundle;
+using Microsoft.Health.Fhir.Core.Features;
+using Microsoft.Health.Fhir.Core.Features.Context;
+using Microsoft.Health.Fhir.Core.Features.Persistence.Orchestration;
+using Microsoft.Health.Fhir.Tests.Common;
+using Microsoft.Health.Test.Utilities;
+using Xunit;
+
+namespace Microsoft.Health.Fhir.Api.UnitTests.Features.Headers
+{
+ [Trait(Traits.OwningTeam, OwningTeam.Fhir)]
+ [Trait(Traits.Category, Categories.Web)]
+ public sealed class HttpContextExtensionsTests
+ {
+ [Fact]
+ public void WhenHttpContextDoesNotHaveCustomHeaders_ReturnDefaultValues()
+ {
+ HttpContext httpContext = GetFakeHttpContext();
+
+ bool isLatencyOverEfficiencyEnabled = httpContext.IsLatencyOverEfficiencyEnabled();
+ Assert.False(isLatencyOverEfficiencyEnabled);
+
+ BundleProcessingLogic bundleProcessingLogic = httpContext.GetBundleProcessingLogic();
+ Assert.Equal(BundleProcessingLogic.Sequential, bundleProcessingLogic);
+
+ // #conditionalQueryParallelism
+ ConditionalQueryProcessingLogic conditionalQueryProcessingLogic = httpContext.GetConditionalQueryProcessingLogic();
+ Assert.Equal(ConditionalQueryProcessingLogic.Sequential, conditionalQueryProcessingLogic);
+ }
+
+ [Theory]
+ [InlineData("", false)]
+ [InlineData(null, false)]
+ [InlineData("false", false)]
+ [InlineData("falsE", false)]
+ [InlineData("FALSE", false)]
+ [InlineData("2112", false)]
+ [InlineData("true", true)]
+ [InlineData("true ", true)]
+ [InlineData("TRUE", true)]
+ [InlineData(" TRUE ", true)]
+ [InlineData(" tRuE ", true)]
+ public void WhenHttpContextHasCustomHeaders_ReturnIfLatencyOverEfficiencyIsEnabled(string value, bool isEnabled)
+ {
+ var httpHeaders = new Dictionary() { { KnownHeaders.QueryLatencyOverEfficiency, value } };
+ HttpContext httpContext = GetFakeHttpContext(httpHeaders);
+
+ bool isLatencyOverEfficiencyEnabled = httpContext.IsLatencyOverEfficiencyEnabled();
+
+ Assert.Equal(isEnabled, isLatencyOverEfficiencyEnabled);
+ }
+
+ [Theory]
+ [InlineData("", ConditionalQueryProcessingLogic.Sequential)]
+ [InlineData(null, ConditionalQueryProcessingLogic.Sequential)]
+ [InlineData("sequential", ConditionalQueryProcessingLogic.Sequential)]
+ [InlineData("sequential ", ConditionalQueryProcessingLogic.Sequential)]
+ [InlineData("Sequential", ConditionalQueryProcessingLogic.Sequential)]
+ [InlineData("2112", ConditionalQueryProcessingLogic.Sequential)]
+ [InlineData("red barchetta", ConditionalQueryProcessingLogic.Sequential)]
+ [InlineData("parallel", ConditionalQueryProcessingLogic.Parallel)]
+ [InlineData("parallel ", ConditionalQueryProcessingLogic.Parallel)]
+ [InlineData("Parallel", ConditionalQueryProcessingLogic.Parallel)]
+ [InlineData(" pArAllEl ", ConditionalQueryProcessingLogic.Parallel)]
+ [InlineData("PARALLEL", ConditionalQueryProcessingLogic.Parallel)]
+ public void WhenHttpContextHasCustomHeaders_ReturnIfConditionalQueryProcessingLogicIsSet(string value, ConditionalQueryProcessingLogic processingLogic)
+ {
+ // #conditionalQueryParallelism
+
+ var httpHeaders = new Dictionary() { { KnownHeaders.ConditionalQueryProcessingLogic, value } };
+ HttpContext httpContext = GetFakeHttpContext(httpHeaders);
+
+ ConditionalQueryProcessingLogic conditionalQueryProcessingLogic = httpContext.GetConditionalQueryProcessingLogic();
+
+ Assert.Equal(processingLogic, conditionalQueryProcessingLogic);
+ }
+
+ [Theory]
+ [InlineData("", BundleProcessingLogic.Sequential)]
+ [InlineData(null, BundleProcessingLogic.Sequential)]
+ [InlineData("sequential", BundleProcessingLogic.Sequential)]
+ [InlineData("sequential ", BundleProcessingLogic.Sequential)]
+ [InlineData("Sequential", BundleProcessingLogic.Sequential)]
+ [InlineData("2112", BundleProcessingLogic.Sequential)]
+ [InlineData("red barchetta", BundleProcessingLogic.Sequential)]
+ [InlineData("parallel", BundleProcessingLogic.Parallel)]
+ [InlineData("parallel ", BundleProcessingLogic.Parallel)]
+ [InlineData("Parallel", BundleProcessingLogic.Parallel)]
+ [InlineData(" pArAllEl ", BundleProcessingLogic.Parallel)]
+ [InlineData("PARALLEL", BundleProcessingLogic.Parallel)]
+ public void WhenHttpContextHasCustomHeaders_ReturnIfBundleProcessingLogicIsSet(string value, BundleProcessingLogic processingLogic)
+ {
+ var httpHeaders = new Dictionary() { { BundleOrchestratorNamingConventions.HttpHeaderBundleProcessingLogic, value } };
+ HttpContext httpContext = GetFakeHttpContext(httpHeaders);
+
+ BundleProcessingLogic bundleProcessingLogic = httpContext.GetBundleProcessingLogic();
+
+ Assert.Equal(processingLogic, bundleProcessingLogic);
+ }
+
+ [Fact]
+ public void WhenProvidedAFhirRequestContext_ThenDecorateItWithOptimizeConcurrency()
+ {
+ // #conditionalQueryParallelism
+
+ IFhirRequestContext fhirRequestContext = new Core.UnitTests.Features.Context.DefaultFhirRequestContext()
+ {
+ BaseUri = new Uri("https://localhost/"),
+ CorrelationId = Guid.NewGuid().ToString(),
+ ResponseHeaders = new HeaderDictionary(),
+ RequestHeaders = new HeaderDictionary(),
+ };
+
+ fhirRequestContext.DecorateRequestContextWithOptimizedConcurrency();
+
+ Assert.True(fhirRequestContext.Properties.ContainsKey(KnownQueryParameterNames.OptimizeConcurrency));
+ Assert.Equal(true, fhirRequestContext.Properties[KnownQueryParameterNames.OptimizeConcurrency]);
+ }
+
+ private static HttpContext GetFakeHttpContext(IReadOnlyDictionary optionalHttpHeaders = default)
+ {
+ var httpContext = new DefaultHttpContext()
+ {
+ Request =
+ {
+ Scheme = "https",
+ Host = new HostString("localhost"),
+ PathBase = new PathString("/"),
+ },
+ };
+
+ if (optionalHttpHeaders != null)
+ {
+ foreach (var header in optionalHttpHeaders)
+ {
+ httpContext.Request.Headers.Append(header.Key, header.Value);
+ }
+ }
+
+ return httpContext;
+ }
+ }
+}
diff --git a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Resources/Bundle/BundleHandlerEdgeCaseTests.cs b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Resources/Bundle/BundleHandlerEdgeCaseTests.cs
index 11e010672a..4a8c937d9e 100644
--- a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Resources/Bundle/BundleHandlerEdgeCaseTests.cs
+++ b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Resources/Bundle/BundleHandlerEdgeCaseTests.cs
@@ -11,6 +11,7 @@
using System.IO;
using System.Linq;
using System.Threading;
+using Azure;
using Hl7.Fhir.Model;
using Hl7.Fhir.Serialization;
using Hl7.Fhir.Specification.Navigation;
@@ -87,9 +88,9 @@ public void GivenABundle_WhenProcessedWithConditionalQueryMaxParallelism_TheFhir
// 4 - If the created HTTP request does not contain the header "x-conditionalquery-processing-logic" set as "parallel", then the key "_optimizeConcurrency"
// is not expected in the FHIR Request Context property bag.
- var bundleHandlerComponents = GetBundleHandlerComponents(new BundleRequestOptions() { MaxParallelism = maxParallelism });
+ var requestContext = CreateRequestContextForBundleHandlerProcessing(new BundleRequestOptions() { MaxParallelism = maxParallelism });
- var fhirContextPropertyBag = bundleHandlerComponents.FhirRequestContext.Properties;
+ var fhirContextPropertyBag = requestContext.Properties;
if (maxParallelism)
{
@@ -102,7 +103,7 @@ public void GivenABundle_WhenProcessedWithConditionalQueryMaxParallelism_TheFhir
}
}
- private (IRouter Router, BundleConfiguration BundleConfiguration, IMediator Mediator, BundleHandler BundleHandler, IFhirRequestContext FhirRequestContext) GetBundleHandlerComponents(BundleRequestOptions options)
+ private IFhirRequestContext CreateRequestContextForBundleHandlerProcessing(BundleRequestOptions options)
{
IRouter router = Substitute.For();
@@ -147,6 +148,11 @@ public void GivenABundle_WhenProcessedWithConditionalQueryMaxParallelism_TheFhir
httpContext.Request.Headers[KnownHeaders.ConditionalQueryProcessingLogic] = new StringValues("parallel");
}
+ if (options.QueryLatencyOverEfficiency)
+ {
+ httpContext.Request.Headers[KnownHeaders.QueryLatencyOverEfficiency] = new StringValues("true");
+ }
+
httpContextAccessor.HttpContext.Returns(httpContext);
var transactionHandler = Substitute.For();
@@ -174,7 +180,7 @@ public void GivenABundle_WhenProcessedWithConditionalQueryMaxParallelism_TheFhir
mediator,
NullLogger.Instance);
- return (router, bundleConfiguration, mediator, bundleHandler, fhirRequestContextAccessor.RequestContext);
+ return fhirRequestContextAccessor.RequestContext;
}
private IFeatureCollection CreateFeatureCollection(IRouter router)
@@ -214,6 +220,8 @@ private IFeatureCollection CreateFeatureCollection(IRouter router)
private sealed class BundleRequestOptions()
{
public bool MaxParallelism { get; set; } = false;
+
+ public bool QueryLatencyOverEfficiency { get; set; } = false;
}
}
}
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 e1e051edbf..d95f4f111d 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
@@ -13,6 +13,7 @@
+
@@ -34,6 +35,7 @@
+
@@ -51,6 +53,7 @@
+
diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Controllers/FhirController.cs b/src/Microsoft.Health.Fhir.Shared.Api/Controllers/FhirController.cs
index a662da0c91..d2bb4d821f 100644
--- a/src/Microsoft.Health.Fhir.Shared.Api/Controllers/FhirController.cs
+++ b/src/Microsoft.Health.Fhir.Shared.Api/Controllers/FhirController.cs
@@ -59,6 +59,7 @@ namespace Microsoft.Health.Fhir.Api.Controllers
[ServiceFilter(typeof(AuditLoggingFilterAttribute))]
[ServiceFilter(typeof(OperationOutcomeExceptionFilterAttribute))]
[ServiceFilter(typeof(ValidateFormatParametersAttribute))]
+ [ServiceFilter(typeof(QueryLatencyOverEfficiencyFilterAttribute))]
[ValidateResourceTypeFilter]
[ValidateModelState]
public class FhirController : Controller
@@ -181,7 +182,7 @@ public async Task ConditionalCreate([FromBody] Resource resource)
{
StringValues conditionalCreateHeader = HttpContext.Request.Headers[KnownHeaders.IfNoneExist];
- SetupRequestContextWithConditionalQueryMaxParallelism();
+ SetupConditionalRequestWithQueryOptimizeConcurrency();
Tuple[] conditionalParameters = QueryHelpers.ParseQuery(conditionalCreateHeader)
.SelectMany(query => query.Value, (query, value) => Tuple.Create(query.Key, value)).ToArray();
@@ -230,7 +231,7 @@ public async Task Update([FromBody] Resource resource, [ModelBind
[AuditEventType(AuditEventSubType.ConditionalUpdate)]
public async Task ConditionalUpdate([FromBody] Resource resource)
{
- SetupRequestContextWithConditionalQueryMaxParallelism();
+ SetupConditionalRequestWithQueryOptimizeConcurrency();
IReadOnlyList> conditionalParameters = GetQueriesForSearch();
@@ -435,7 +436,7 @@ public async Task ConditionalDelete(string typeParameter, [FromQu
{
IReadOnlyList> conditionalParameters = GetQueriesForSearch();
- SetupRequestContextWithConditionalQueryMaxParallelism();
+ SetupConditionalRequestWithQueryOptimizeConcurrency();
DeleteResourceResponse response = await _mediator.Send(
new ConditionalDeleteResourceRequest(
@@ -496,7 +497,7 @@ public async Task ConditionalPatchJson(string typeParameter, [Fro
IReadOnlyList> conditionalParameters = GetQueriesForSearch();
var payload = new JsonPatchPayload(patchDocument);
- SetupRequestContextWithConditionalQueryMaxParallelism();
+ SetupConditionalRequestWithQueryOptimizeConcurrency();
UpsertResourceResponse response = await _mediator.ConditionalPatchResourceAsync(
new ConditionalPatchResourceRequest(typeParameter, payload, conditionalParameters, GetBundleResourceContext(), ifMatchHeader),
@@ -541,7 +542,7 @@ public async Task ConditionalPatchFhir(string typeParameter, [Fro
IReadOnlyList> conditionalParameters = GetQueriesForSearch();
var payload = new FhirPathPatchPayload(paramsResource);
- SetupRequestContextWithConditionalQueryMaxParallelism();
+ SetupConditionalRequestWithQueryOptimizeConcurrency();
UpsertResourceResponse response = await _mediator.ConditionalPatchResourceAsync(
new ConditionalPatchResourceRequest(typeParameter, payload, conditionalParameters, GetBundleResourceContext(), ifMatchHeader),
@@ -690,7 +691,7 @@ private BundleResourceContext GetBundleResourceContext()
return null;
}
- private void SetupRequestContextWithConditionalQueryMaxParallelism()
+ private void SetupConditionalRequestWithQueryOptimizeConcurrency()
{
if (HttpContext?.Request?.Headers != null && _fhirRequestContextAccessor != null)
{
diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Features/Filters/QueryLatencyOverEfficiencyFilterAttribute.cs b/src/Microsoft.Health.Fhir.Shared.Api/Features/Filters/QueryLatencyOverEfficiencyFilterAttribute.cs
new file mode 100644
index 0000000000..51bef05f08
--- /dev/null
+++ b/src/Microsoft.Health.Fhir.Shared.Api/Features/Filters/QueryLatencyOverEfficiencyFilterAttribute.cs
@@ -0,0 +1,61 @@
+// -------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
+// -------------------------------------------------------------------------------------------------
+
+using System;
+using EnsureThat;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc.Filters;
+using Microsoft.Health.Core.Features.Context;
+using Microsoft.Health.Fhir.Api.Features.Headers;
+using Microsoft.Health.Fhir.Core.Features.Context;
+using Microsoft.Health.Fhir.Core.Registration;
+
+namespace Microsoft.Health.Fhir.Api.Features.Filters
+{
+ ///
+ /// Latency over efficiency filter.
+ /// Adds to FHIR Request Context a flag to optimize query latency over efficiency.
+ ///
+ [AttributeUsage(AttributeTargets.Class)]
+ public sealed class QueryLatencyOverEfficiencyFilterAttribute : ActionFilterAttribute
+ {
+ private readonly RequestContextAccessor _fhirRequestContextAccessor;
+ private readonly IFhirRuntimeConfiguration _runtimeConfiguration;
+
+ public QueryLatencyOverEfficiencyFilterAttribute(RequestContextAccessor fhirRequestContextAccessor, IFhirRuntimeConfiguration runtimeConfiguration)
+ {
+ EnsureArg.IsNotNull(fhirRequestContextAccessor, nameof(fhirRequestContextAccessor));
+ EnsureArg.IsNotNull(runtimeConfiguration, nameof(runtimeConfiguration));
+
+ _fhirRequestContextAccessor = fhirRequestContextAccessor;
+ _runtimeConfiguration = runtimeConfiguration;
+ }
+
+ public override void OnActionExecuting(ActionExecutingContext context)
+ {
+ EnsureArg.IsNotNull(context, nameof(context));
+
+ if (_runtimeConfiguration.IsLatencyOverEfficiencySupported)
+ {
+ SetupConditionalRequestWithQueryOptimizeConcurrency(context.HttpContext, _fhirRequestContextAccessor.RequestContext);
+ }
+
+ base.OnActionExecuting(context);
+ }
+
+ private static void SetupConditionalRequestWithQueryOptimizeConcurrency(HttpContext context, IFhirRequestContext fhirRequestContext)
+ {
+ if (context?.Request?.Headers != null && fhirRequestContext != null)
+ {
+ bool latencyOverEfficiencyEnabled = context.IsLatencyOverEfficiencyEnabled();
+
+ if (latencyOverEfficiencyEnabled)
+ {
+ fhirRequestContext.DecorateRequestContextWithOptimizedConcurrency();
+ }
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Features/Headers/HttpContextExtensions.cs b/src/Microsoft.Health.Fhir.Shared.Api/Features/Headers/HttpContextExtensions.cs
index 11766b6ebd..f0e24a4b68 100644
--- a/src/Microsoft.Health.Fhir.Shared.Api/Features/Headers/HttpContextExtensions.cs
+++ b/src/Microsoft.Health.Fhir.Shared.Api/Features/Headers/HttpContextExtensions.cs
@@ -19,19 +19,19 @@ namespace Microsoft.Health.Fhir.Api.Features.Headers
public static class HttpContextExtensions
{
///
- /// Retrieves from the HTTP header information about the conditional-query processing logic to be adopted.
+ /// Retrieves from the HTTP header if "Latency over efficiency" is enabled.
///
/// HTTP context
- public static ConditionalQueryProcessingLogic GetConditionalQueryProcessingLogic(this HttpContext outerHttpContext)
+ public static bool IsLatencyOverEfficiencyEnabled(this HttpContext outerHttpContext)
{
- var defaultValue = ConditionalQueryProcessingLogic.Sequential;
+ const bool defaultValue = false;
if (outerHttpContext == null)
{
return defaultValue;
}
- if (outerHttpContext.Request.Headers.TryGetValue(KnownHeaders.ConditionalQueryProcessingLogic, out StringValues headerValues))
+ if (outerHttpContext.Request.Headers.TryGetValue(KnownHeaders.QueryLatencyOverEfficiency, out StringValues headerValues))
{
string processingLogicAsString = headerValues.FirstOrDefault();
if (string.IsNullOrWhiteSpace(processingLogicAsString))
@@ -39,27 +39,48 @@ public static ConditionalQueryProcessingLogic GetConditionalQueryProcessingLogic
return defaultValue;
}
- ConditionalQueryProcessingLogic processingLogic = (ConditionalQueryProcessingLogic)Enum.Parse(typeof(ConditionalQueryProcessingLogic), processingLogicAsString.Trim(), ignoreCase: true);
- return processingLogic;
+ if (bool.TryParse(headerValues.ToString().Trim(), out bool result))
+ {
+ return result;
+ }
}
return defaultValue;
}
+ ///
+ /// Retrieves from the HTTP header information about the conditional-query processing logic to be adopted.
+ ///
+ /// HTTP context
+ public static ConditionalQueryProcessingLogic GetConditionalQueryProcessingLogic(this HttpContext outerHttpContext)
+ {
+ return ExtractEnumerationFlagFromHttpHeader(
+ outerHttpContext,
+ httpHeaderName: KnownHeaders.ConditionalQueryProcessingLogic,
+ defaultValue: ConditionalQueryProcessingLogic.Sequential);
+ }
+
///
/// Retrieves from the HTTP header information about the bundle processing logic to be adopted.
///
/// HTTP context
public static BundleProcessingLogic GetBundleProcessingLogic(this HttpContext outerHttpContext)
{
- var defaultValue = BundleProcessingLogic.Sequential;
+ return ExtractEnumerationFlagFromHttpHeader(
+ outerHttpContext,
+ httpHeaderName: BundleOrchestratorNamingConventions.HttpHeaderBundleProcessingLogic,
+ defaultValue: BundleProcessingLogic.Sequential);
+ }
+ public static TEnum ExtractEnumerationFlagFromHttpHeader(HttpContext outerHttpContext, string httpHeaderName, TEnum defaultValue)
+ where TEnum : struct, Enum
+ {
if (outerHttpContext == null)
{
return defaultValue;
}
- if (outerHttpContext.Request.Headers.TryGetValue(BundleOrchestratorNamingConventions.HttpHeaderBundleProcessingLogic, out StringValues headerValues))
+ if (outerHttpContext.Request.Headers.TryGetValue(httpHeaderName, out StringValues headerValues))
{
string processingLogicAsString = headerValues.FirstOrDefault();
if (string.IsNullOrWhiteSpace(processingLogicAsString))
@@ -67,8 +88,10 @@ public static BundleProcessingLogic GetBundleProcessingLogic(this HttpContext ou
return defaultValue;
}
- BundleProcessingLogic processingLogic = (BundleProcessingLogic)Enum.Parse(typeof(BundleProcessingLogic), processingLogicAsString.Trim(), ignoreCase: true);
- return processingLogic;
+ if (Enum.TryParse(processingLogicAsString.Trim(), ignoreCase: true, out TEnum result) && Enum.IsDefined(result))
+ {
+ return result;
+ }
}
return defaultValue;
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 0e45425f7a..33ca37b553 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
@@ -85,7 +85,7 @@ public partial class BundleHandler : IRequestHandler Handle(BundleRequest request, CancellationToken cancellationToken)
@@ -243,7 +243,7 @@ public async Task Handle(BundleRequest request, CancellationToke
}
}
- private static ConditionalQueryProcessingLogic SetRequestContextWithConditionalQueryProcessingLogic(HttpContext outerHttpContext, IFhirRequestContext fhirRequestContext, ILogger logger)
+ private static bool SetRequestContextWithOptimizedQuerying(HttpContext outerHttpContext, IFhirRequestContext fhirRequestContext, ILogger logger)
{
try
{
@@ -252,16 +252,15 @@ private static ConditionalQueryProcessingLogic SetRequestContextWithConditionalQ
if (conditionalQueryProcessingLogic == ConditionalQueryProcessingLogic.Parallel)
{
fhirRequestContext.DecorateRequestContextWithOptimizedConcurrency();
+ return true;
}
-
- return conditionalQueryProcessingLogic;
}
catch (Exception e)
{
logger.LogWarning(e, "Error while extracting the Conditional-Query Processing Logic out of the HTTP Header: {ErrorMessage}", e.Message);
}
- return ConditionalQueryProcessingLogic.Sequential;
+ return false;
}
private BundleProcessingLogic GetBundleProcessingLogic(HttpContext outerHttpContext, ILogger logger)
@@ -921,7 +920,7 @@ private static OperationOutcome CreateOperationOutcome(OperationOutcome.IssueSev
private BundleHandlerStatistics CreateNewBundleHandlerStatistics(BundleProcessingLogic processingLogic)
{
- BundleHandlerStatistics statistics = new BundleHandlerStatistics(_bundleType, processingLogic, _conditionalQueryProcessingLogic, _requestCount);
+ BundleHandlerStatistics statistics = new BundleHandlerStatistics(_bundleType, processingLogic, _optimizedQuerySet, _requestCount);
statistics.StartCollectingResults();
diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/Bundle/BundleHandlerStatistics.cs b/src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/Bundle/BundleHandlerStatistics.cs
index 0805034d72..5e72ab2e89 100644
--- a/src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/Bundle/BundleHandlerStatistics.cs
+++ b/src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/Bundle/BundleHandlerStatistics.cs
@@ -24,13 +24,13 @@ internal sealed class BundleHandlerStatistics : BaseOperationStatistics
public BundleHandlerStatistics(
BundleType? bundleType,
BundleProcessingLogic bundleProcessingLogic,
- ConditionalQueryProcessingLogic conditionalQueryProcessingLogic,
+ bool optimizedQuerySet,
int numberOfResources)
: base()
{
BundleType = bundleType;
BundleProcessingLogic = bundleProcessingLogic;
- ConditionalQueryProcessingLogic = conditionalQueryProcessingLogic;
+ OptimizedQueryProcessing = optimizedQuerySet;
NumberOfResources = numberOfResources;
_entries = new List();
}
@@ -41,7 +41,7 @@ public BundleHandlerStatistics(
public BundleProcessingLogic BundleProcessingLogic { get; }
- public ConditionalQueryProcessingLogic ConditionalQueryProcessingLogic { get; }
+ public bool OptimizedQueryProcessing { get; }
public override string GetLoggingCategory() => LoggingCategory;
@@ -61,7 +61,7 @@ public override string GetStatisticsAsJson()
label = GetLoggingCategory(),
bundleType = BundleType.ToString(),
processingLogic = BundleProcessingLogic.ToString(),
- conditionalQuery = ConditionalQueryProcessingLogic.ToString(),
+ optimizedQuerySet = OptimizedQueryProcessing.ToString(),
numberOfResources = NumberOfResources,
executionTime = ElapsedMilliseconds,
success = successedRequests,
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 00242a0448..61ba295b49 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
@@ -26,6 +26,7 @@
+
diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Modules/FhirModule.cs b/src/Microsoft.Health.Fhir.Shared.Api/Modules/FhirModule.cs
index 03f6bb1f77..fc262bb3f0 100644
--- a/src/Microsoft.Health.Fhir.Shared.Api/Modules/FhirModule.cs
+++ b/src/Microsoft.Health.Fhir.Shared.Api/Modules/FhirModule.cs
@@ -108,6 +108,7 @@ ResourceElement SetMetadata(Resource resource, string versionId, DateTimeOffset
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
+ services.AddSingleton();
// Support for resolve()
FhirPathCompiler.DefaultSymbolTable.AddFhirExtensions();