Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved binary support #2771

Merged
merged 8 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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