Skip to content

Commit

Permalink
Added support for searching reference by id, type/id, or url. (#5)
Browse files Browse the repository at this point in the history
Added support for searching reference by id, type/id, or url.
  • Loading branch information
jackliums authored Sep 21, 2018
1 parent 23581f9 commit bf97bdf
Show file tree
Hide file tree
Showing 59 changed files with 1,063 additions and 404 deletions.
108 changes: 79 additions & 29 deletions docs/Search.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,39 @@ System and code will be stored as string values and quantity value will be store

#### ReferenceSearchValue

Reference value will be stored as is. We can also apply some normalization logic in here in the future. For example, if the reference is specified using external reference (e.g., full URL) and the domain matches the current server, we might choose to store the reference as normalized internal reference so that even if the domain name changes, the reference continues to work.
The resource could contain a reference represented by a relative URL (e.g., Patient/123) or an absolute URL (e.g., <http://example.com/Patient/123>). An absolute URL could be either referencing a "external" resource (e.g., resource that exists on an external system) or referencing a "internal" resource (e.g., resource that exist within the current system). This can be determined by matching the service base URL.

The reference can be searched in 3 ways: id, type/id, or url. The following examples illustrate how the search should behave based on the data stored and the search criteria.

Assuming our service is running at http://xyz.com/ and we have the following references stored:

1. Patient/123
2. <http://xyz.com/Patient/123>
3. <http://abc.com/Patient/123>
4. Device/123

_Theoretically, #1 and #2 are equivalent since they are both referencing the same internal resource. They are both valid references. The advantage/disadvantage is discussed [here](http://hl7.org/fhir/stu3/references.html#literal)._

|Search value |Matches |Comment|
|----------------------------|----------|-------|
|abc | |It does not match to anything.|
|123 |1, 2, 3, 4|The resource id needs to match.|
|Patient/123 |1, 2, 3 |The resource type and resource id need to match.
|<http://xyz.com/Patient/123>|1, 2 |Since this is an absolute URL with the service base URL matching the current server, it will match internal reference both stored in relative URL format as well as absolute URL format.|
|<http://abc.com/Patient/123>|3 |Since this is an absolute URL pointing to an external resource, it will only match that reference.|

In order to support this, we need to parse the reference into multiple parts so that they can be searched efficiently. The parsing is using a modified regular expression based on the [published version](http://hl7.org/fhir/stu3/references.html#literal).

Using the examples above:

|Reference value |Kind |BaseUri |ReferenceType|ResourceId|Comment|
|----------------------------|------------------|-----------------|-------------|----------|-------|
|Patient/123 |InternalOrExternal| |Patient |123 | |
|<http://xyz.com/Patient/123>|Internal | |Patient |123 |Because the service base URL matches the current server, the BaseUri will not be populated to indicate that this is an internal reference. The Kind will be Internal so that we don't match resources that exists on another server.|
|<http://abc.com/Patient/123>|External |<http://abc.com/>|Patient |123 | |
|Device/123 |InternalOrExternal| |Device |123 | |

_One thing to note is that with this approach if the service base URL changes (e.g., change in the domain name) and there is an internal reference with absolute URL using the old domain name, then the search using the absolute URL with the new service base URL will find resources that contains the absolute URL with the old domain name._

#### StringSearchValue

Expand Down Expand Up @@ -91,32 +123,32 @@ To convert from FHIR element to `ISearchValue` type, various converters have bee

The current list of supported converters are:

|FHIR element type|ISearchValue Type|
|-----------------|-----------------|
|Address|StringSearchValue|
|CodeableConcept|TokenSearchValue|
|Code&lt;T&gt;|TokenSearchValue|
|Code|TokenSearchValue|
|ContactPoint|TokenSearchValue|
|Date|DateTimeSearchValue|
|FhirBoolean|TokenSearchValue|
|FhirDateTime|DateTimeSearchValue|
|FhirDecimal|NumberSearchValue|
|FhirString|StringSearchValue|
|FhirUri|UriSearchValue|
|HumanName|StringSearchValue|
|Identifier|TokenSearchValue|
|Id|TokenSearchValue|
|Instant|DateTimeSearchValue|
|Integer|NumberSearchValue|
|Markdown|StringSearchValue|
|Oid|TokenSearchValue|
|Period|DateTimeSearchValue|
|PositiveInt|TokenSearchValue|
|Quantity|QuantitySearchValue|
|FHIR element type|ISearchValue Type |
|-----------------|--------------------|
|Address |StringSearchValue |
|CodeableConcept |TokenSearchValue |
|Code&lt;T&gt; |TokenSearchValue |
|Code |TokenSearchValue |
|ContactPoint |TokenSearchValue |
|Date |DateTimeSearchValue |
|FhirBoolean |TokenSearchValue |
|FhirDateTime |DateTimeSearchValue |
|FhirDecimal |NumberSearchValue |
|FhirString |StringSearchValue |
|FhirUri |UriSearchValue |
|HumanName |StringSearchValue |
|Identifier |TokenSearchValue |
|Id |TokenSearchValue |
|Instant |DateTimeSearchValue |
|Integer |NumberSearchValue |
|Markdown |StringSearchValue |
|Oid |TokenSearchValue |
|Period |DateTimeSearchValue |
|PositiveInt |TokenSearchValue |
|Quantity |QuantitySearchValue |
|ResourceReference|ReferenceSearchValue|
|SimpleQuantity|QuantitySearchValue|
|UnsignedInt|NumberSearchValue|
|SimpleQuantity |QuantitySearchValue |
|UnsignedInt |NumberSearchValue |

If the converter does not exist, the server will write a warning message instead of failing the request.

Expand Down Expand Up @@ -258,7 +290,7 @@ The number field is `n`.

#### Serializing QuantitySearchValue

The system field is `s`, code field is `c`, and the quantity field is `q`.
The system field is `s`, the code field is `c`, and the quantity field is `q`.

``` json
{
Expand All @@ -271,12 +303,30 @@ The system field is `s`, code field is `c`, and the quantity field is `q`.

#### Serializing ReferenceSearchValue

The reference field is `r`.
Reference can be searched in multiple ways:

The base URI field is `rb`, the resource type field is `rt`, and the resource id is `ri`.

The base URI is normalized so that it can be searched later.

In the case of internal reference:

``` json
{
"p": "performer",
"rt": "Practitioner",
"ri": "123"
}
```

In the case of external reference:

``` json
{
"p": "performer",
"r": "Practitioner/example"
"rb": "http://example.com/stu3/",
"rt": "Practitioner",
"ri": "123"
}
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

using System;
using MediatR;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
Expand All @@ -22,7 +21,7 @@ public class FhirControllerTests
{
private readonly IMediator _mediator = Substitute.For<IMediator>();
private readonly ILogger<FhirController> _logger = NullLogger<FhirController>.Instance;
private readonly IFhirContextAccessor _contextAccessor = Substitute.For<IFhirContextAccessor>();
private readonly IFhirRequestContextAccessor _contextAccessor = Substitute.For<IFhirRequestContextAccessor>();
private readonly IUrlResolver _urlResolver = Substitute.For<IUrlResolver>();
private readonly FeatureConfiguration _featureConfiguration = new FeatureConfiguration();

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// -------------------------------------------------------------------------------------------------
// 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.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Health.Fhir.Api.Features.Context;
using Microsoft.Health.Fhir.Core.Features.Context;
using NSubstitute;
using Xunit;

namespace Microsoft.Health.Fhir.Api.UnitTests.Features.Context
{
public class FhirRequestContextMiddlewareTests
{
[Fact]
public async Task GivenAnHttpRequest_WhenExecutingFhirRequestContextMiddleware_ThenCorrectRequestTypeShouldBeSet()
{
IFhirRequestContext fhirRequestContext = await SetupAsync(CreateHttpContext());

Assert.NotNull(fhirRequestContext.RequestType);

Assert.Equal(ValueSets.AuditEventType.RestFulOperation.System, fhirRequestContext.RequestType.System);
Assert.Equal(ValueSets.AuditEventType.RestFulOperation.Code, fhirRequestContext.RequestType.Code);
}

[Fact]
public async Task GivenAnHttpRequest_WhenExecutingFhirRequestContextMiddleware_ThenCorrectUriShouldBeSet()
{
IFhirRequestContext fhirRequestContext = await SetupAsync(CreateHttpContext());

Assert.Equal(new Uri("https://localhost:30/stu3/Observation?code=123"), fhirRequestContext.Uri);
}

[Fact]
public async Task GivenAnHttpRequest_WhenExecutingFhirRequestContextMiddleware_ThenCorrectBaseUriShouldBeSet()
{
IFhirRequestContext fhirRequestContext = await SetupAsync(CreateHttpContext());

Assert.Equal(new Uri("https://localhost:30/stu3"), fhirRequestContext.BaseUri);
}

[Fact]
public async Task GivenAnHttpContextWithPrincipal_WhenExecutingFhirRequestContextMiddleware_ThenPrincipalShouldBeSet()
{
HttpContext httpContext = CreateHttpContext();

var principal = new ClaimsPrincipal();

httpContext.User = principal;

IFhirRequestContext fhirRequestContext = await SetupAsync(httpContext);

Assert.Same(principal, fhirRequestContext.Principal);
}

private async Task<IFhirRequestContext> SetupAsync(HttpContext httpContext)
{
var fhirRequestContextAccessor = Substitute.For<IFhirRequestContextAccessor>();
var fhirContextMiddlware = new FhirRequestContextMiddleware(next: (innerHttpContext) => Task.CompletedTask);
string Provider() => Guid.NewGuid().ToString();

await fhirContextMiddlware.Invoke(httpContext, fhirRequestContextAccessor, Provider);

Assert.NotNull(fhirRequestContextAccessor.FhirRequestContext);

return fhirRequestContextAccessor.FhirRequestContext;
}

private HttpContext CreateHttpContext()
{
HttpContext httpContext = new DefaultHttpContext();

httpContext.Request.Method = "GET";
httpContext.Request.Scheme = "https";
httpContext.Request.Host = new HostString("localhost", 30);
httpContext.Request.PathBase = new PathString("/stu3");
httpContext.Request.Path = new PathString("/Observation");
httpContext.Request.QueryString = new QueryString("?code=123");

return httpContext;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public class BaseExceptionMiddlewareTests
{
private readonly string _correlationId;
private readonly DefaultHttpContext _context;
private readonly IFhirContextAccessor _fhirContextAccessor;
private readonly IFhirRequestContextAccessor _fhirRequestContextAccessor;
private readonly FhirJsonSerializer _fhirJsonSerializer;
private readonly FhirXmlSerializer _fhirXmlSerializer;
private readonly CorrelationIdProvider _provider = () => Guid.NewGuid().ToString();
Expand All @@ -31,8 +31,8 @@ public BaseExceptionMiddlewareTests()
{
_correlationId = Guid.NewGuid().ToString();

_fhirContextAccessor = Substitute.For<IFhirContextAccessor>();
_fhirContextAccessor.FhirContext.CorrelationId.Returns(_correlationId);
_fhirRequestContextAccessor = Substitute.For<IFhirRequestContextAccessor>();
_fhirRequestContextAccessor.FhirRequestContext.CorrelationId.Returns(_correlationId);
_fhirJsonSerializer = new FhirJsonSerializer();
_fhirXmlSerializer = new FhirXmlSerializer();

Expand All @@ -48,7 +48,7 @@ public BaseExceptionMiddlewareTests()
[InlineData("The MetadataAddress or Authority must use HTTPS unless disabled for development by setting RequireHttpsMetadata=false.", "The security configuration requires the authority to be set to an https address.")]
public async Task WhenExecutingBaseExceptionMiddleware_GivenAnHttpContextWithException_TheResponseShouldBeOperationOutcome(string exceptionMessage, string diagnosticMessage)
{
var baseExceptionMiddleware = new BaseExceptionMiddleware(innerHttpContext => throw new Exception(exceptionMessage), NullLogger<BaseExceptionMiddleware>.Instance, _fhirContextAccessor, _fhirJsonSerializer, _fhirXmlSerializer, _provider);
var baseExceptionMiddleware = new BaseExceptionMiddleware(innerHttpContext => throw new Exception(exceptionMessage), NullLogger<BaseExceptionMiddleware>.Instance, _fhirRequestContextAccessor, _fhirJsonSerializer, _fhirXmlSerializer, _provider);

await baseExceptionMiddleware.Invoke(_context);

Expand Down Expand Up @@ -76,7 +76,7 @@ public async Task WhenExecutingBaseExceptionMiddleware_GivenAnHttpContextWithExc
[Fact]
public async Task WhenExecutingBaseExceptionMiddleware_GivenAnHttpContextWithNoException_TheResponseShouldBeEmpty()
{
var baseExceptionMiddleware = new BaseExceptionMiddleware(innerHttpContext => Task.CompletedTask, NullLogger<BaseExceptionMiddleware>.Instance, _fhirContextAccessor, _fhirJsonSerializer, _fhirXmlSerializer, _provider);
var baseExceptionMiddleware = new BaseExceptionMiddleware(innerHttpContext => Task.CompletedTask, NullLogger<BaseExceptionMiddleware>.Instance, _fhirRequestContextAccessor, _fhirJsonSerializer, _fhirXmlSerializer, _provider);

await baseExceptionMiddleware.Invoke(_context);

Expand Down
Loading

0 comments on commit bf97bdf

Please sign in to comment.