Skip to content

Commit

Permalink
Merge pull request #2771 from FirelyTeam/feature/improved-binary-support
Browse files Browse the repository at this point in the history
Improved binary support
  • Loading branch information
ewoutkramer authored Apr 23, 2024
2 parents ca7c44d + d269b74 commit 95ce7c1
Show file tree
Hide file tree
Showing 11 changed files with 298 additions and 134 deletions.
41 changes: 24 additions & 17 deletions src/Hl7.Fhir.Base/Rest/BaseFhirClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -799,15 +799,17 @@ public async Task DeleteHistoryVersionAsync(string location, CancellationToken?
await verifyServerVersion(cancellation).ConfigureAwait(false);

var request = tx.Entry[0];
var maybeBinaryInteraction = new ResourceIdentity(request.Request.Url).ResourceType == "Binary";
var requestMessage = request.ToHttpRequestMessage(
Requester.BaseUrl,
getSerializationEngine(),
Settings.UseFhirVersionInAcceptHeader ? fhirVersion : null,
Settings);
Settings,
maybeBinaryInteraction);

using var responseMessage = await Requester.ExecuteAsync(requestMessage, cancellation).ConfigureAwait(false);

return await extractResourceFromHttpResponse<TResource>(expect, responseMessage, entryComponent: request);
return await extractResourceFromHttpResponse<TResource>(expect, responseMessage, entryComponent: request, useBinaryProtocol: maybeBinaryInteraction);
}

private async Task<TResource?> executeAsync<TResource>(HttpRequestMessage request, IEnumerable<HttpStatusCode> expect, CancellationToken? ct) where TResource : Resource
Expand Down Expand Up @@ -854,14 +856,14 @@ private static ResourceIdentity verifyResourceIdentity(Uri location, bool needId
}

// either msg or entryComponent should be set
private async Task<TResource?> extractResourceFromHttpResponse<TResource>(IEnumerable<HttpStatusCode> expect, HttpResponseMessage responseMessage, HttpRequestMessage? msg = null, Bundle.EntryComponent? entryComponent = null) where TResource : Resource
private async Task<TResource?> extractResourceFromHttpResponse<TResource>(IEnumerable<HttpStatusCode> expect, HttpResponseMessage responseMessage, HttpRequestMessage? msg = null, Bundle.EntryComponent? entryComponent = null, bool useBinaryProtocol = false) where TResource : Resource
{
if (msg is null && entryComponent is null) throw new ArgumentException("Either msg or entryComponent should be set");
// Validate the response and throw the appropriate exceptions. Also, if we have *not* verified the FHIR version
// of the server, add a suggestion about this in the (legacy) parsing exception.
var suggestedVersionOnParseError = !Settings.VerifyFhirVersion ? fhirVersion : null;
(LastResult, LastBody, LastBodyAsText, LastBodyAsResource, var issue) =
await ValidateResponse(responseMessage, expect, getSerializationEngine(), suggestedVersionOnParseError)
await ValidateResponse(responseMessage, expect, getSerializationEngine(), suggestedVersionOnParseError, useBinaryProtocol)
.ConfigureAwait(false);

// If an error occurred while trying to interpret and validate the response, we will bail out now.
Expand Down Expand Up @@ -913,19 +915,24 @@ static string unexpectedBodyTypeForMessage(HttpRequestMessage msg) => $"Operatio
$"expected a body of type {typeof(TResource).Name} but a {typeof(TResource).Name} was returned.";
}

/// <summary>
/// Validates the <see cref="HttpResponseMessage"/> and throws the appropriate exceptions.
/// It also simulates the exception-throwing behaviour of the original TypedElement-based parsers.
/// </summary>
/// <exception cref="FhirOperationException">The body content type could not be handled or the response status indicated failure, or we received an unexpected success status.</exception>
/// <exception cref="FormatException">Thrown when the original ITypedElement-based parsers are used and a parse exception occurred.</exception>
/// <exception cref="DeserializationFailedException">Thrown when a newer parsers is used and a parse exception occurred.</exception>
/// <seealso cref="HttpContentParsers.ExtractResponseData(HttpResponseMessage, IFhirSerializationEngine)"/>
internal static async Task<ResponseData> ValidateResponse(HttpResponseMessage responseMessage, IEnumerable<HttpStatusCode> expect, IFhirSerializationEngine engine, string? suggestedVersionOnParseError)
{
var responseData = (await responseMessage.ExtractResponseData(engine).ConfigureAwait(false))
.TranslateUnsupportedBodyTypeException(responseMessage.StatusCode)
.TranslateLegacyParserException(suggestedVersionOnParseError);
/// <summary>
/// Validates the <see cref="HttpResponseMessage"/> and throws the appropriate exceptions.
/// It also simulates the exception-throwing behaviour of the original TypedElement-based parsers.
/// </summary>
/// <exception cref="FhirOperationException">The body content type could not be handled or the response status indicated failure, or we received an unexpected success status.</exception>
/// <exception cref="FormatException">Thrown when the original ITypedElement-based parsers are used and a parse exception occurred.</exception>
/// <exception cref="DeserializationFailedException">Thrown when a newer parsers is used and a parse exception occurred.</exception>
/// <seealso cref="HttpContentParsers.ExtractResponseData(HttpResponseMessage, IFhirSerializationEngine, bool)"/>
internal static async Task<ResponseData> ValidateResponse(
HttpResponseMessage responseMessage,
IEnumerable<HttpStatusCode> expect,
IFhirSerializationEngine engine,
string? suggestedVersionOnParseError,
bool useBinaryProtocol)
{
var responseData = (await responseMessage.ExtractResponseData(engine, useBinaryProtocol).ConfigureAwait(false))
.TranslateUnsupportedBodyTypeException(responseMessage.StatusCode)
.TranslateLegacyParserException(suggestedVersionOnParseError);

// If extracting the data caused an issue, return it immediately
if (responseData.Issue is not null)
Expand Down
24 changes: 15 additions & 9 deletions src/Hl7.Fhir.Base/Rest/EntryToHttpExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,38 +12,44 @@
using Hl7.Fhir.Serialization;
using Hl7.Fhir.Utility;
using System;
using System.Linq;
using System.Net.Http;

namespace Hl7.Fhir.Rest
{


internal static class EntryToHttpExtensions
{
{
public static HttpRequestMessage ToHttpRequestMessage(
this Bundle.EntryComponent entry,
Uri baseUrl,
IFhirSerializationEngine ser,
string? fhirVersion,
FhirClientSettings settings)
FhirClientSettings settings,
bool binaryEndpoint = false)
{
var request = entry.Request;
var interaction = entry.Annotation<InteractionType>();
var method = request.Method?.toHttpMethod(interaction)
?? throw new ArgumentException("EntryComponent should specify a Request.Method.", nameof(request));
var serialization = settings.PreferredFormat;

var uri = getRequestUrl(request.Url, baseUrl);
var uri = getRequestUrl(baseUrl, request.Url);

var message = new HttpRequestMessage(method, uri);

if (!(binaryEndpoint && settings.BinaryReceivePreference == BinaryTransferBehaviour.UseData))
{
message = settings.UseFormatParameter
? message.WithFormatParameter(serialization)
: message.WithAccept(serialization, fhirVersion, settings.PreferCompressedResponses);
}

message = setBody(message)
.WithDefaultAgent()
.WithPreconditions(request.IfMatch, request.IfNoneMatch, request.IfModifiedSince, request.IfNoneExist);

message = settings.UseFormatParameter
? message.WithFormatParameter(serialization)
: message.WithAccept(serialization, fhirVersion, settings.PreferCompressedResponses);

bool canHaveReturnPreference = interaction is InteractionType.Create or InteractionType.Update or InteractionType.Patch or InteractionType.Transaction;

if (canHaveReturnPreference)
Expand All @@ -63,7 +69,7 @@ HttpRequestMessage setBody(HttpRequestMessage message)

message = entry.Resource switch
{
Binary bin => message.WithBinaryContent(bin),
Binary binaryData when settings.BinarySendBehaviour is BinaryTransferBehaviour.UseData => message.WithBinaryContent(binaryData),
Parameters pars when isSearchUsingPost => message.WithFormUrlEncodedParameters(pars),
Resource resource => message.WithResourceContent(resource, serialization, ser, fhirVersion),
null => message.WithNoBody()
Expand All @@ -73,7 +79,7 @@ HttpRequestMessage setBody(HttpRequestMessage message)
}
}

private static Uri getRequestUrl(string requestUrl, Uri baseUrl)
private static Uri getRequestUrl(Uri baseUrl, string requestUrl)
{
// Create an absolute uri when the interaction.Url is relative.
var uri = new Uri(
Expand Down
30 changes: 30 additions & 0 deletions src/Hl7.Fhir.Base/Rest/FhirClientSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

#nullable enable

using Hl7.Fhir.Model;
using Hl7.Fhir.Serialization;
using Hl7.Fhir.Utility;
using System;
Expand Down Expand Up @@ -128,6 +129,17 @@ public bool CompressRequestBody
/// </summary>
public ParserSettings? ParserSettings = ParserSettings.CreateDefault();

/// <summary>
/// How to transfer binary data when sending data to a Binary endpoint.
/// </summary>
public BinaryTransferBehaviour BinarySendBehaviour = BinaryTransferBehaviour.UseResource;

/// <summary>
/// Whether we ask the server to return us binary data or a Binary resource.
/// </summary>
public BinaryTransferBehaviour BinaryReceivePreference = BinaryTransferBehaviour.UseData;


public FhirClientSettings() { }

/// <summary>Clone constructor. Generates a new <see cref="FhirClientSettings"/> instance initialized from the state of the specified instance.</summary>
Expand Down Expand Up @@ -158,6 +170,8 @@ public void CopyTo(FhirClientSettings other)
other.PreferredParameterHandling = PreferredParameterHandling;
other.SerializationEngine = SerializationEngine;
other.RequestBodyCompressionMethod = RequestBodyCompressionMethod;
other.BinaryReceivePreference = BinaryReceivePreference;
other.BinarySendBehaviour = BinarySendBehaviour;
}

/// <summary>Creates a new <see cref="FhirClientSettings"/> object that is a copy of the current instance.</summary>
Expand All @@ -166,6 +180,22 @@ public void CopyTo(FhirClientSettings other)
/// <summary>Creates a new <see cref="FhirClientSettings"/> instance with default property values.</summary>
public static FhirClientSettings CreateDefault() => new();
}

/// <summary>
/// Describes how the client sends and receives data at the Binary endpoint.
/// </summary>
public enum BinaryTransferBehaviour
{
/// <summary>
/// Prefer to package binary data in a <see cref="Binary"/> resource.
/// </summary>
UseResource,

/// <summary>
/// Prefer to send and receive the binary data directly to and from the endpoint.
/// </summary>
UseData
}
}

#nullable restore
10 changes: 5 additions & 5 deletions src/Hl7.Fhir.Base/Rest/HttpContentBuilders.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,15 @@
#nullable enable

using Hl7.Fhir.Model;
using Hl7.Fhir.Serialization;
using Hl7.Fhir.Utility;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Linq;
using Hl7.Fhir.Serialization;
using System;
using System.Text.Unicode;
using System.Text;
using System.Net;

namespace Hl7.Fhir.Rest
{
Expand All @@ -34,6 +33,7 @@ public static HttpContent CreateContentFromBinary(Binary b)
{
var content = new ByteArrayContent(b.Data ?? b.Content);
content.Headers.ContentType = MediaTypeHeaderValue.Parse(b.ContentType);
content.Headers.LastModified = b.Meta?.LastUpdated;

if (b.SecurityContext?.Reference is { } secRef)
content.Headers.Add(HttpUtil.SECURITYCONTEXT, secRef);
Expand Down
Loading

0 comments on commit 95ce7c1

Please sign in to comment.