diff --git a/src/Microsoft.Health.Fhir.Api/Features/ActionResults/ResourceActionResult.cs b/src/Microsoft.Health.Fhir.Api/Features/ActionResults/ResourceActionResult.cs
index ae187d7557..5c37edf93b 100644
--- a/src/Microsoft.Health.Fhir.Api/Features/ActionResults/ResourceActionResult.cs
+++ b/src/Microsoft.Health.Fhir.Api/Features/ActionResults/ResourceActionResult.cs
@@ -65,7 +65,7 @@ public override async Task ExecuteResultAsync(ActionContext context)
}
catch (ObjectDisposedException ode)
{
- throw new ServiceUnavailableException(Resources.NotAbleToCreateTheFinalResultsOfAnOperation, ode);
+ throw new ServiceUnavailableException(Api.Resources.NotAbleToCreateTheFinalResultsOfAnOperation, ode);
}
HttpResponse response = context.HttpContext.Response;
diff --git a/src/Microsoft.Health.Fhir.Api/Features/ActionResults/TooManyRequestsActionResult.cs b/src/Microsoft.Health.Fhir.Api/Features/ActionResults/TooManyRequestsActionResult.cs
index 28bc594f42..5ea3122906 100644
--- a/src/Microsoft.Health.Fhir.Api/Features/ActionResults/TooManyRequestsActionResult.cs
+++ b/src/Microsoft.Health.Fhir.Api/Features/ActionResults/TooManyRequestsActionResult.cs
@@ -17,7 +17,7 @@ public TooManyRequestsActionResult()
new OperationOutcomeIssue(
OperationOutcomeConstants.IssueSeverity.Error,
OperationOutcomeConstants.IssueType.Throttled,
- Resources.TooManyConcurrentRequests),
+ Api.Resources.TooManyConcurrentRequests),
HttpStatusCode.TooManyRequests)
{
}
diff --git a/src/Microsoft.Health.Fhir.Api/Features/Filters/QueryCacheFilterAttribute.cs b/src/Microsoft.Health.Fhir.Api/Features/Filters/QueryCacheFilterAttribute.cs
new file mode 100644
index 0000000000..25a50e5047
--- /dev/null
+++ b/src/Microsoft.Health.Fhir.Api/Features/Filters/QueryCacheFilterAttribute.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 QueryCacheFilterAttribute : ActionFilterAttribute
+ {
+ private readonly RequestContextAccessor _fhirRequestContextAccessor;
+ private readonly IFhirRuntimeConfiguration _runtimeConfiguration;
+
+ public QueryCacheFilterAttribute(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.IsQueryCacheSupported)
+ {
+ SetupConditionalRequestWithQueryCache(context.HttpContext, _fhirRequestContextAccessor.RequestContext);
+ }
+
+ base.OnActionExecuting(context);
+ }
+
+ private static void SetupConditionalRequestWithQueryCache(HttpContext context, IFhirRequestContext fhirRequestContext)
+ {
+ if (context?.Request?.Headers != null && fhirRequestContext != null)
+ {
+ string useQueryCache = context.GetQueryCache();
+
+ if (!string.IsNullOrEmpty(useQueryCache))
+ {
+ fhirRequestContext.DecorateRequestContextWithQueryCache(useQueryCache);
+ }
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateAsyncRequestFilterAttribute.cs b/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateAsyncRequestFilterAttribute.cs
index 1ebf91e912..2d23c81d1d 100644
--- a/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateAsyncRequestFilterAttribute.cs
+++ b/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateAsyncRequestFilterAttribute.cs
@@ -28,7 +28,7 @@ public override void OnActionExecuting(ActionExecutingContext context)
if (!context.HttpContext.Request.Headers.TryGetValue(KnownHeaders.Prefer, out var preferHeaderValue) ||
!string.Equals(preferHeaderValue[0], PreferHeaderExpectedValue, StringComparison.OrdinalIgnoreCase))
{
- throw new RequestNotValidException(string.Format(Resources.UnsupportedHeaderValue, preferHeaderValue.FirstOrDefault(), KnownHeaders.Prefer));
+ throw new RequestNotValidException(string.Format(Api.Resources.UnsupportedHeaderValue, preferHeaderValue.FirstOrDefault(), KnownHeaders.Prefer));
}
}
}
diff --git a/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateExportRequestFilterAttribute.cs b/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateExportRequestFilterAttribute.cs
index 77db0a8bd2..8c18b3d2f1 100644
--- a/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateExportRequestFilterAttribute.cs
+++ b/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateExportRequestFilterAttribute.cs
@@ -65,7 +65,7 @@ public override void OnActionExecuting(ActionExecutingContext context)
acceptHeaderValue.Count != 1 ||
!string.Equals(acceptHeaderValue[0], KnownContentTypes.JsonContentType, StringComparison.OrdinalIgnoreCase))
{
- throw new RequestNotValidException(string.Format(Resources.UnsupportedHeaderValue, acceptHeaderValue.FirstOrDefault(), HeaderNames.Accept));
+ throw new RequestNotValidException(string.Format(Api.Resources.UnsupportedHeaderValue, acceptHeaderValue.FirstOrDefault(), HeaderNames.Accept));
}
if (context.HttpContext.Request.Headers.TryGetValue(PreferHeaderName, out var preferHeaderValues))
@@ -79,7 +79,7 @@ public override void OnActionExecuting(ActionExecutingContext context)
|| (v.Length == 1 && !(requiredHeaderValueFound = string.Equals(v[0], PreferHeaderValueRequired, StringComparison.OrdinalIgnoreCase)))
|| (v.Length == 2 && (!string.Equals(v[0], PreferHeaderValueOptional, StringComparison.OrdinalIgnoreCase) || !Enum.TryParse(v[1], true, out _))))
{
- throw new RequestNotValidException(string.Format(Resources.UnsupportedHeaderValue, value, PreferHeaderName));
+ throw new RequestNotValidException(string.Format(Api.Resources.UnsupportedHeaderValue, value, PreferHeaderName));
}
}
@@ -102,14 +102,14 @@ public override void OnActionExecuting(ActionExecutingContext context)
continue;
}
- throw new RequestNotValidException(string.Format(Resources.UnsupportedParameter, paramName));
+ throw new RequestNotValidException(string.Format(Api.Resources.UnsupportedParameter, paramName));
}
if (queryCollection?.Keys != null &&
queryCollection.Keys.Contains(KnownQueryParameterNames.TypeFilter) &&
!queryCollection.Keys.Contains(KnownQueryParameterNames.Type))
{
- throw new RequestNotValidException(Resources.TypeFilterWithoutTypeIsUnsupported);
+ throw new RequestNotValidException(Api.Resources.TypeFilterWithoutTypeIsUnsupported);
}
if (queryCollection.TryGetValue(KnownQueryParameterNames.OutputFormat, out var outputFormats))
@@ -118,7 +118,7 @@ public override void OnActionExecuting(ActionExecutingContext context)
{
if (!(outputFormat == null || SupportedOutputFormats.Contains(outputFormat)))
{
- throw new RequestNotValidException(string.Format(Resources.InvalidOutputFormat, outputFormat));
+ throw new RequestNotValidException(string.Format(Api.Resources.InvalidOutputFormat, outputFormat));
}
}
}
diff --git a/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateFormatParametersAttribute.cs b/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateFormatParametersAttribute.cs
index dc51d2f779..e9af338d63 100644
--- a/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateFormatParametersAttribute.cs
+++ b/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateFormatParametersAttribute.cs
@@ -51,13 +51,13 @@ public override async Task OnActionExecutionAsync(ActionExecutingContext context
{
if (!await _parametersValidator.IsFormatSupportedAsync(headerValue[0]))
{
- throw new UnsupportedMediaTypeException(string.Format(Resources.UnsupportedHeaderValue, headerValue.FirstOrDefault(), HeaderNames.ContentType));
+ throw new UnsupportedMediaTypeException(string.Format(Api.Resources.UnsupportedHeaderValue, headerValue.FirstOrDefault(), HeaderNames.ContentType));
}
}
else
{
// If no content type is supplied, then the server should respond with an unsupported media type exception.
- throw new UnsupportedMediaTypeException(Resources.ContentTypeHeaderRequired);
+ throw new UnsupportedMediaTypeException(Api.Resources.ContentTypeHeaderRequired);
}
}
else if (httpContext.Request.Method.Equals(HttpMethod.Patch.Method, StringComparison.OrdinalIgnoreCase))
@@ -66,7 +66,7 @@ public override async Task OnActionExecutionAsync(ActionExecutingContext context
{
if (!await _parametersValidator.IsPatchFormatSupportedAsync(headerValue[0]))
{
- throw new UnsupportedMediaTypeException(string.Format(Resources.UnsupportedHeaderValue, headerValue.FirstOrDefault(), HeaderNames.ContentType));
+ throw new UnsupportedMediaTypeException(string.Format(Api.Resources.UnsupportedHeaderValue, headerValue.FirstOrDefault(), HeaderNames.ContentType));
}
}
}
diff --git a/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateImportRequestFilterAttribute.cs b/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateImportRequestFilterAttribute.cs
index c4146c06a6..3d31306e25 100644
--- a/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateImportRequestFilterAttribute.cs
+++ b/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateImportRequestFilterAttribute.cs
@@ -35,7 +35,7 @@ public override void OnActionExecuting(ActionExecutingContext context)
preferHeaderValue.Count != 1 ||
!string.Equals(preferHeaderValue[0], PreferHeaderExpectedValue, StringComparison.OrdinalIgnoreCase))
{
- throw new RequestNotValidException(string.Format(Resources.UnsupportedHeaderValue, preferHeaderValue.FirstOrDefault(), PreferHeaderName));
+ throw new RequestNotValidException(string.Format(Api.Resources.UnsupportedHeaderValue, preferHeaderValue.FirstOrDefault(), PreferHeaderName));
}
if (string.Equals(context.HttpContext.Request.Method, "POST", StringComparison.OrdinalIgnoreCase))
@@ -44,7 +44,7 @@ public override void OnActionExecuting(ActionExecutingContext context)
contentTypeHeaderValue.Count != 1 ||
!contentTypeHeaderValue[0].Contains(ContentTypeHeaderExpectedValue, StringComparison.OrdinalIgnoreCase))
{
- throw new RequestNotValidException(string.Format(Resources.UnsupportedHeaderValue, contentTypeHeaderValue.FirstOrDefault(), HeaderNames.ContentType));
+ throw new RequestNotValidException(string.Format(Api.Resources.UnsupportedHeaderValue, contentTypeHeaderValue.FirstOrDefault(), HeaderNames.ContentType));
}
}
}
diff --git a/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateParametersResourceAttribute.cs b/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateParametersResourceAttribute.cs
index 9b08cf1095..49020c7b16 100644
--- a/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateParametersResourceAttribute.cs
+++ b/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateParametersResourceAttribute.cs
@@ -25,12 +25,12 @@ public override void OnActionExecuting(ActionExecutingContext context)
context.ActionArguments?.TryGetValue("inputParams", out inputResource);
if (inputResource == null)
{
- throw new RequestNotValidException(Resources.MissingInputParams);
+ throw new RequestNotValidException(Api.Resources.MissingInputParams);
}
if (inputResource is not Parameters)
{
- throw new RequestNotValidException(string.Format(Resources.UnsupportedResourceType, inputResource.GetType().ToString()));
+ throw new RequestNotValidException(string.Format(Api.Resources.UnsupportedResourceType, inputResource.GetType().ToString()));
}
}
}
diff --git a/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateReindexRequestFilterAttribute.cs b/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateReindexRequestFilterAttribute.cs
index b19c62c444..d257de16fc 100644
--- a/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateReindexRequestFilterAttribute.cs
+++ b/src/Microsoft.Health.Fhir.Api/Features/Filters/ValidateReindexRequestFilterAttribute.cs
@@ -28,7 +28,7 @@ public override void OnActionExecuting(ActionExecutingContext context)
if (context.HttpContext.Request.Headers.TryGetValue(PreferHeaderName, out var preferHeaderValue) &&
!string.Equals(preferHeaderValue[0], PreferHeaderExpectedValue, StringComparison.OrdinalIgnoreCase))
{
- throw new RequestNotValidException(string.Format(Resources.UnsupportedHeaderValue, preferHeaderValue.FirstOrDefault(), PreferHeaderName));
+ throw new RequestNotValidException(string.Format(Api.Resources.UnsupportedHeaderValue, preferHeaderValue.FirstOrDefault(), PreferHeaderName));
}
}
}
diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Features/Headers/HttpContextExtensions.cs b/src/Microsoft.Health.Fhir.Api/Features/Headers/HttpContextExtensions.cs
similarity index 82%
rename from src/Microsoft.Health.Fhir.Shared.Api/Features/Headers/HttpContextExtensions.cs
rename to src/Microsoft.Health.Fhir.Api/Features/Headers/HttpContextExtensions.cs
index ba6de951ed..ff390aa122 100644
--- a/src/Microsoft.Health.Fhir.Shared.Api/Features/Headers/HttpContextExtensions.cs
+++ b/src/Microsoft.Health.Fhir.Api/Features/Headers/HttpContextExtensions.cs
@@ -48,6 +48,21 @@ public static bool IsLatencyOverEfficiencyEnabled(this HttpContext outerHttpCont
return defaultValue;
}
+ ///
+ /// Retrieves from the HTTP header information on using query caching.
+ ///
+ /// HTTP context
+ /// Query cache header value
+ public static string GetQueryCache(this HttpContext outerHttpContext)
+ {
+ if (outerHttpContext != null && outerHttpContext.Request.Headers.TryGetValue(KnownHeaders.QueryCacheSetting, out StringValues headerValues))
+ {
+ return headerValues.FirstOrDefault();
+ }
+
+ return null;
+ }
+
///
/// Retrieves from the HTTP header information about the conditional-query processing logic to be adopted.
///
@@ -110,5 +125,15 @@ public static bool DecorateRequestContextWithOptimizedConcurrency(this IFhirRequ
return requestContext.Properties.TryAdd(KnownQueryParameterNames.OptimizeConcurrency, true);
}
+
+ public static bool DecorateRequestContextWithQueryCache(this IFhirRequestContext requestContext, string value)
+ {
+ if (requestContext == null)
+ {
+ return false;
+ }
+
+ return requestContext.Properties.TryAdd(KnownQueryParameterNames.QueryCaching, value);
+ }
}
}
diff --git a/src/Microsoft.Health.Fhir.Api/Features/Operations/Import/InitialImportLockMiddleware.cs b/src/Microsoft.Health.Fhir.Api/Features/Operations/Import/InitialImportLockMiddleware.cs
index c40d8af662..ed99f2f169 100644
--- a/src/Microsoft.Health.Fhir.Api/Features/Operations/Import/InitialImportLockMiddleware.cs
+++ b/src/Microsoft.Health.Fhir.Api/Features/Operations/Import/InitialImportLockMiddleware.cs
@@ -26,7 +26,7 @@ public sealed class InitialImportLockMiddleware
// hard-coding these to minimize resource consumption for locked message
private const string LockedContentType = "application/json; charset=utf-8";
- private static readonly ReadOnlyMemory _lockedBody = CreateLockedBody(Resources.LockedForInitialImportMode);
+ private static readonly ReadOnlyMemory _lockedBody = CreateLockedBody(Api.Resources.LockedForInitialImportMode);
public InitialImportLockMiddleware(
RequestDelegate next,
diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/ConditionalQueryProcessingLogic.cs b/src/Microsoft.Health.Fhir.Api/Features/Resources/ConditionalQueryProcessingLogic.cs
similarity index 100%
rename from src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/ConditionalQueryProcessingLogic.cs
rename to src/Microsoft.Health.Fhir.Api/Features/Resources/ConditionalQueryProcessingLogic.cs
diff --git a/src/Microsoft.Health.Fhir.Api/Features/Routing/SearchPostReroutingMiddleware.cs b/src/Microsoft.Health.Fhir.Api/Features/Routing/SearchPostReroutingMiddleware.cs
index a3c01667a4..2e11c8bb50 100644
--- a/src/Microsoft.Health.Fhir.Api/Features/Routing/SearchPostReroutingMiddleware.cs
+++ b/src/Microsoft.Health.Fhir.Api/Features/Routing/SearchPostReroutingMiddleware.cs
@@ -50,7 +50,7 @@ public async Task Invoke(HttpContext context)
{
context.Response.Clear();
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
- await context.Response.WriteAsync(Resources.ContentTypeFormUrlEncodedExpected);
+ await context.Response.WriteAsync(Api.Resources.ContentTypeFormUrlEncodedExpected);
return;
}
}
diff --git a/src/Microsoft.Health.Fhir.Api/Features/Routing/UrlResolver.cs b/src/Microsoft.Health.Fhir.Api/Features/Routing/UrlResolver.cs
index c2ea11f4b9..3c0fb0b6c4 100644
--- a/src/Microsoft.Health.Fhir.Api/Features/Routing/UrlResolver.cs
+++ b/src/Microsoft.Health.Fhir.Api/Features/Routing/UrlResolver.cs
@@ -260,7 +260,7 @@ public Uri ResolveOperationResultUrl(string operationName, string id)
routeName = RouteNames.GetBulkDeleteStatusById;
break;
default:
- throw new OperationNotImplementedException(string.Format(Resources.OperationNotImplemented, operationName));
+ throw new OperationNotImplementedException(string.Format(Api.Resources.OperationNotImplemented, operationName));
}
var routeValues = new RouteValueDictionary()
@@ -317,7 +317,7 @@ public Uri ResolveOperationDefinitionUrl(string operationName)
routeName = RouteNames.SearchParameterStatusOperationDefinition;
break;
default:
- throw new OperationNotImplementedException(string.Format(Resources.OperationNotImplemented, operationName));
+ throw new OperationNotImplementedException(string.Format(Api.Resources.OperationNotImplemented, operationName));
}
return GetRouteUri(
diff --git a/src/Microsoft.Health.Fhir.Api/Features/SMART/SmartClinicalScopesMiddleware.cs b/src/Microsoft.Health.Fhir.Api/Features/SMART/SmartClinicalScopesMiddleware.cs
index faf69685e8..4905eeaa98 100644
--- a/src/Microsoft.Health.Fhir.Api/Features/SMART/SmartClinicalScopesMiddleware.cs
+++ b/src/Microsoft.Health.Fhir.Api/Features/SMART/SmartClinicalScopesMiddleware.cs
@@ -152,14 +152,14 @@ public async Task Invoke(
{
if (authorizationConfiguration.ErrorOnMissingFhirUserClaim)
{
- throw new BadHttpRequestException(string.Format(Resources.FhirUserClaimMustBeURL, fhirUser));
+ throw new BadHttpRequestException(string.Format(Api.Resources.FhirUserClaimMustBeURL, fhirUser));
}
}
catch (ArgumentNullException)
{
if (authorizationConfiguration.ErrorOnMissingFhirUserClaim)
{
- throw new BadHttpRequestException(Resources.FhirUserClaimCannotBeNull);
+ throw new BadHttpRequestException(Api.Resources.FhirUserClaimCannotBeNull);
}
}
}
diff --git a/src/Microsoft.Health.Fhir.Api/Features/Throttling/ThrottlingMiddleware.cs b/src/Microsoft.Health.Fhir.Api/Features/Throttling/ThrottlingMiddleware.cs
index 947f39ff9d..dd4f4f4e80 100644
--- a/src/Microsoft.Health.Fhir.Api/Features/Throttling/ThrottlingMiddleware.cs
+++ b/src/Microsoft.Health.Fhir.Api/Features/Throttling/ThrottlingMiddleware.cs
@@ -40,7 +40,7 @@ public sealed class ThrottlingMiddleware : IAsyncDisposable, IDisposable
// hard-coding these to minimize resource consumption when throttling
private const string ThrottledContentType = "application/json; charset=utf-8";
- private static readonly ReadOnlyMemory _throttledBody = CreateThrottledBody(Resources.TooManyConcurrentRequests);
+ private static readonly ReadOnlyMemory _throttledBody = CreateThrottledBody(Api.Resources.TooManyConcurrentRequests);
private readonly RequestDelegate _next;
private readonly ILogger _logger;
@@ -284,7 +284,7 @@ private async Task Return429(HttpContext context)
{
Interlocked.Increment(ref _currentPeriodRejectedCount);
- _logger.LogWarning(Resources.TooManyConcurrentRequests + " Limit is {Limit}. Requests in flight {Requests}", _concurrentRequestLimit, _requestsInFlight);
+ _logger.LogWarning(Api.Resources.TooManyConcurrentRequests + " Limit is {Limit}. Requests in flight {Requests}", _concurrentRequestLimit, _requestsInFlight);
context.Response.StatusCode = StatusCodes.Status429TooManyRequests;
diff --git a/src/Microsoft.Health.Fhir.Core/Features/KnownHeaders.cs b/src/Microsoft.Health.Fhir.Core/Features/KnownHeaders.cs
index e289a6012a..57bc665ab0 100644
--- a/src/Microsoft.Health.Fhir.Core/Features/KnownHeaders.cs
+++ b/src/Microsoft.Health.Fhir.Core/Features/KnownHeaders.cs
@@ -34,5 +34,7 @@ public static class KnownHeaders
// #conditionalQueryParallelism - Header used to activate parallel conditional-query processing.
public const string ConditionalQueryProcessingLogic = "x-conditionalquery-processing-logic";
+
+ public const string QueryCacheSetting = "x-ms-query-cache";
}
}
diff --git a/src/Microsoft.Health.Fhir.Core/Features/KnownQueryParameterNames.cs b/src/Microsoft.Health.Fhir.Core/Features/KnownQueryParameterNames.cs
index dfb3ce2fb5..abf121dbac 100644
--- a/src/Microsoft.Health.Fhir.Core/Features/KnownQueryParameterNames.cs
+++ b/src/Microsoft.Health.Fhir.Core/Features/KnownQueryParameterNames.cs
@@ -61,10 +61,18 @@ public static class KnownQueryParameterNames
public const string Container = "_container";
///
- /// Originally for CosmosDB workloads to hint that this request should run with a max parallel setting.
+ /// This setting is currently set by:
+ /// x-ms-query-latency-over-efficiency
+ /// x-conditionalquery-processing-logic
+ /// It is used to hint that the request should run with a max parallel setting.
///
public const string OptimizeConcurrency = "_optimizeConcurrency";
+ ///
+ /// This setting is controlled by the x-ms-query-cache-enabled header. It controls whether to use the query cache or not.
+ ///
+ public const string QueryCaching = "_queryCaching";
+
///
/// The anonymization configuration
///
diff --git a/src/Microsoft.Health.Fhir.Core/Registration/AzureApiForFhirRuntimeConfiguration.cs b/src/Microsoft.Health.Fhir.Core/Registration/AzureApiForFhirRuntimeConfiguration.cs
index 51b29f5b9e..55d30e00d0 100644
--- a/src/Microsoft.Health.Fhir.Core/Registration/AzureApiForFhirRuntimeConfiguration.cs
+++ b/src/Microsoft.Health.Fhir.Core/Registration/AzureApiForFhirRuntimeConfiguration.cs
@@ -20,5 +20,7 @@ public class AzureApiForFhirRuntimeConfiguration : IFhirRuntimeConfiguration
public bool IsTransactionSupported => false;
public bool IsLatencyOverEfficiencySupported => true;
+
+ public bool IsQueryCacheSupported => false;
}
}
diff --git a/src/Microsoft.Health.Fhir.Core/Registration/AzureHealthDataServicesRuntimeConfiguration.cs b/src/Microsoft.Health.Fhir.Core/Registration/AzureHealthDataServicesRuntimeConfiguration.cs
index 26ff55233a..e22302fb69 100644
--- a/src/Microsoft.Health.Fhir.Core/Registration/AzureHealthDataServicesRuntimeConfiguration.cs
+++ b/src/Microsoft.Health.Fhir.Core/Registration/AzureHealthDataServicesRuntimeConfiguration.cs
@@ -20,5 +20,7 @@ public class AzureHealthDataServicesRuntimeConfiguration : IFhirRuntimeConfigura
public bool IsTransactionSupported => true;
public bool IsLatencyOverEfficiencySupported => false;
+
+ public bool IsQueryCacheSupported => true;
}
}
diff --git a/src/Microsoft.Health.Fhir.Core/Registration/IFhirRuntimeConfiguration.cs b/src/Microsoft.Health.Fhir.Core/Registration/IFhirRuntimeConfiguration.cs
index 94411f0c67..1160dd1ba9 100644
--- a/src/Microsoft.Health.Fhir.Core/Registration/IFhirRuntimeConfiguration.cs
+++ b/src/Microsoft.Health.Fhir.Core/Registration/IFhirRuntimeConfiguration.cs
@@ -33,5 +33,10 @@ public interface IFhirRuntimeConfiguration
/// Supports the 'latency-over-efficiency' HTTP header.
///
bool IsLatencyOverEfficiencySupported { get; }
+
+ ///
+ /// Supports the query cache HTTP header.
+ ///
+ bool IsQueryCacheSupported { get; }
}
}
diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Controllers/FhirController.cs b/src/Microsoft.Health.Fhir.Shared.Api/Controllers/FhirController.cs
index cee02b0442..c96b18e870 100644
--- a/src/Microsoft.Health.Fhir.Shared.Api/Controllers/FhirController.cs
+++ b/src/Microsoft.Health.Fhir.Shared.Api/Controllers/FhirController.cs
@@ -63,6 +63,7 @@ namespace Microsoft.Health.Fhir.Api.Controllers
[ServiceFilter(typeof(OperationOutcomeExceptionFilterAttribute))]
[ServiceFilter(typeof(ValidateFormatParametersAttribute))]
[ServiceFilter(typeof(QueryLatencyOverEfficiencyFilterAttribute))]
+ [ServiceFilter(typeof(QueryCacheFilterAttribute))]
[ValidateResourceTypeFilter]
[ValidateModelState]
public class FhirController : Controller
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 0acaed0a36..4184a19ce9 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
@@ -36,7 +36,6 @@
-
@@ -44,7 +43,6 @@
-
diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Modules/FhirModule.cs b/src/Microsoft.Health.Fhir.Shared.Api/Modules/FhirModule.cs
index c79a4e4849..2c1610f15e 100644
--- a/src/Microsoft.Health.Fhir.Shared.Api/Modules/FhirModule.cs
+++ b/src/Microsoft.Health.Fhir.Shared.Api/Modules/FhirModule.cs
@@ -116,6 +116,7 @@ ResourceElement SetMetadata(Resource resource, string versionId, DateTimeOffset
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
+ services.AddSingleton();
// Support for resolve()
FhirPathCompiler.DefaultSymbolTable.AddFhirExtensions();
diff --git a/src/Microsoft.Health.Fhir.SqlServer/Features/Search/QueryCacheSetting.cs b/src/Microsoft.Health.Fhir.SqlServer/Features/Search/QueryCacheSetting.cs
new file mode 100644
index 0000000000..d5cbeafd79
--- /dev/null
+++ b/src/Microsoft.Health.Fhir.SqlServer/Features/Search/QueryCacheSetting.cs
@@ -0,0 +1,15 @@
+// -------------------------------------------------------------------------------------------------
+// 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.SqlServer.Features.Search
+{
+ internal class QueryCacheSetting
+ {
+ public const string Enabled = "enabled";
+ public const string Disabled = "disabled";
+ public const string Default = "default";
+ public const string Both = "both";
+ }
+}
diff --git a/src/Microsoft.Health.Fhir.SqlServer/Features/Search/SqlServerSearchService.cs b/src/Microsoft.Health.Fhir.SqlServer/Features/Search/SqlServerSearchService.cs
index 25d9eade9c..97b65c9c7b 100644
--- a/src/Microsoft.Health.Fhir.SqlServer/Features/Search/SqlServerSearchService.cs
+++ b/src/Microsoft.Health.Fhir.SqlServer/Features/Search/SqlServerSearchService.cs
@@ -144,7 +144,7 @@ public override async Task SearchAsync(SearchOptions searchOptions
{
SqlSearchOptions sqlSearchOptions = new SqlSearchOptions(searchOptions);
- SearchResult searchResult = await SearchImpl(sqlSearchOptions, cancellationToken);
+ SearchResult searchResult = await RunSearch(sqlSearchOptions, cancellationToken);
int resultCount = searchResult.Results.Count(r => r.SearchEntryMode == SearchEntryMode.Match);
if (!sqlSearchOptions.IsSortWithFilter &&
@@ -179,7 +179,7 @@ public override async Task SearchAsync(SearchOptions searchOptions
sqlSearchOptions.SortQuerySecondPhase = true;
sqlSearchOptions.MaxItemCount -= resultCount;
- searchResult = await SearchImpl(sqlSearchOptions, cancellationToken);
+ searchResult = await RunSearch(sqlSearchOptions, cancellationToken);
finalResultsInOrder.AddRange(searchResult.Results);
searchResult = new SearchResult(
@@ -208,7 +208,7 @@ public override async Task SearchAsync(SearchOptions searchOptions
sqlSearchOptions.CountOnly = true;
// And perform a second read.
- var countOnlySearchResult = await SearchImpl(sqlSearchOptions, cancellationToken);
+ var countOnlySearchResult = await RunSearch(sqlSearchOptions, cancellationToken);
searchResult.TotalCount = countOnlySearchResult.TotalCount;
}
@@ -223,8 +223,53 @@ public override async Task SearchAsync(SearchOptions searchOptions
return searchResult;
}
- private async Task SearchImpl(SqlSearchOptions sqlSearchOptions, CancellationToken cancellationToken)
+ private async Task RunSearch(SqlSearchOptions sqlSearchOptions, CancellationToken cancellationToken)
{
+ var fhirContext = _requestContextAccessor.RequestContext;
+ if (fhirContext != null
+ && fhirContext.Properties.TryGetValue(KnownQueryParameterNames.QueryCaching, out object useQueryCacheObj)
+ && useQueryCacheObj != null)
+ {
+ var useQueryCache = Convert.ToString(useQueryCacheObj);
+ if (string.Equals(useQueryCache, QueryCacheSetting.Enabled, StringComparison.OrdinalIgnoreCase))
+ {
+ return await SearchImpl(sqlSearchOptions, true, cancellationToken);
+ }
+ else if (string.Equals(useQueryCache, QueryCacheSetting.Disabled, StringComparison.OrdinalIgnoreCase))
+ {
+ return await SearchImpl(sqlSearchOptions, false, cancellationToken);
+ }
+ else if (string.Equals(useQueryCache, QueryCacheSetting.Both, StringComparison.OrdinalIgnoreCase))
+ {
+ _logger.LogInformation("Running search with and without query cache.");
+ var stopwatch = Stopwatch.StartNew();
+
+ using var tokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+ var token = tokenSource.Token;
+
+ var tryWithQueryCache = SearchImpl(sqlSearchOptions, true, token);
+ var tryWithoutQueryCache = SearchImpl(sqlSearchOptions, false, token);
+
+ var result = await Task.WhenAny(tryWithQueryCache, tryWithoutQueryCache);
+ await tokenSource.CancelAsync();
+
+ _logger.LogInformation("First search completed in {ElapsedMilliseconds}ms, query cache enabled: {QueryCacheEnabled}.", stopwatch.ElapsedMilliseconds, result == tryWithQueryCache);
+ return await result;
+ }
+ else // equals default or an invalid value
+ {
+ return await SearchImpl(sqlSearchOptions, _reuseQueryPlans.IsEnabled(_sqlRetryService), cancellationToken);
+ }
+ }
+ else
+ {
+ return await SearchImpl(sqlSearchOptions, _reuseQueryPlans.IsEnabled(_sqlRetryService), cancellationToken);
+ }
+ }
+
+ private async Task SearchImpl(SqlSearchOptions sqlSearchOptions, bool reuseQueryPlans, CancellationToken cancellationToken)
+ {
+ Stopwatch stopwatch = Stopwatch.StartNew();
Expression searchExpression = sqlSearchOptions.Expression;
// AND in the continuation token
@@ -357,7 +402,7 @@ await _sqlRetryService.ExecuteSql(
new HashingSqlQueryParameterManager(new SqlQueryParameterManager(sqlCommand.Parameters)),
_model,
_schemaInformation,
- _reuseQueryPlans.IsEnabled(_sqlRetryService),
+ reuseQueryPlans,
sqlException);
expression.AcceptVisitor(queryGenerator, clonedSearchOptions);
@@ -570,6 +615,7 @@ await _sqlRetryService.ExecuteSql(
cancellationToken,
true); // this enables reads from replicas
+ _logger.LogInformation("Search completed in {ElapsedMilliseconds}ms, query cache enabled: {QueryCacheEnabled}.", stopwatch.ElapsedMilliseconds, reuseQueryPlans);
return searchResult;
}