diff --git a/src/Spark.Engine/Extensions/NetCore/IServiceCollectionExtensions.cs b/src/Spark.Engine/Extensions/NetCore/IServiceCollectionExtensions.cs index 36ef7488d..bd97ab853 100644 --- a/src/Spark.Engine/Extensions/NetCore/IServiceCollectionExtensions.cs +++ b/src/Spark.Engine/Extensions/NetCore/IServiceCollectionExtensions.cs @@ -99,12 +99,24 @@ public static IMvcCoreBuilder AddFhirFormatters(this IServiceCollection services return services.AddMvcCore(options => { - options.InputFormatters.Add(new ResourceJsonInputFormatter(new FhirJsonParser(settings.ParserSettings), ArrayPool.Shared)); - options.InputFormatters.Add(new ResourceXmlInputFormatter(new FhirXmlParser(settings.ParserSettings))); - options.InputFormatters.Add(new BinaryInputFormatter()); - options.OutputFormatters.Add(new ResourceJsonOutputFormatter()); - options.OutputFormatters.Add(new ResourceXmlOutputFormatter()); - options.OutputFormatters.Add(new BinaryOutputFormatter()); + if (settings.UseAsynchronousIO) + { + options.InputFormatters.Add(new AsyncResourceJsonInputFormatter(new FhirJsonParser(settings.ParserSettings))); + options.InputFormatters.Add(new AsyncResourceXmlInputFormatter(new FhirXmlParser(settings.ParserSettings))); + options.InputFormatters.Add(new BinaryInputFormatter()); + options.OutputFormatters.Add(new AsyncResourceJsonOutputFormatter()); + options.OutputFormatters.Add(new AsyncResourceXmlOutputFormatter()); + options.OutputFormatters.Add(new BinaryOutputFormatter()); + } + else + { + options.InputFormatters.Add(new ResourceJsonInputFormatter(new FhirJsonParser(settings.ParserSettings), ArrayPool.Shared)); + options.InputFormatters.Add(new ResourceXmlInputFormatter(new FhirXmlParser(settings.ParserSettings))); + options.InputFormatters.Add(new BinaryInputFormatter()); + options.OutputFormatters.Add(new ResourceJsonOutputFormatter()); + options.OutputFormatters.Add(new ResourceXmlOutputFormatter()); + options.OutputFormatters.Add(new BinaryOutputFormatter()); + } options.RespectBrowserAcceptHeader = true; diff --git a/src/Spark.Engine/Formatters/NetCore/AsyncResourceJsonInputFormatter.cs b/src/Spark.Engine/Formatters/NetCore/AsyncResourceJsonInputFormatter.cs new file mode 100644 index 000000000..72cdd50ff --- /dev/null +++ b/src/Spark.Engine/Formatters/NetCore/AsyncResourceJsonInputFormatter.cs @@ -0,0 +1,64 @@ +#if NETSTANDARD2_0 +using Hl7.Fhir.Model; +using Hl7.Fhir.Serialization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.AspNetCore.WebUtilities; +using Newtonsoft.Json; +using Spark.Core; +using Spark.Engine.Extensions; +using System; +using System.Buffers; +using System.Diagnostics; +using System.IO; +using System.Text; +using System.Threading.Tasks; + +namespace Spark.Engine.Formatters +{ + public class AsyncResourceJsonInputFormatter : TextInputFormatter + { + private readonly FhirJsonParser _parser; + + public AsyncResourceJsonInputFormatter(FhirJsonParser parser) + { + _parser = parser ?? throw new ArgumentNullException(nameof(parser)); + + SupportedEncodings.Clear(); + SupportedEncodings.Add(Encoding.UTF8); + + SupportedMediaTypes.Add("application/json"); + SupportedMediaTypes.Add("application/fhir+json"); + SupportedMediaTypes.Add("application/json+fhir"); + SupportedMediaTypes.Add("text/json"); + } + + protected override bool CanReadType(Type type) + { + return typeof(Resource).IsAssignableFrom(type); + } + + public override async Task ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding) + { + if (context == null) throw new ArgumentNullException(nameof(context)); + if (encoding == null) throw new ArgumentNullException(nameof(encoding)); + if (encoding != Encoding.UTF8) + throw Error.BadRequest("FHIR supports UTF-8 encoding exclusively, not " + encoding.WebName); + + try + { + using var reader = new StreamReader(context.HttpContext.Request.Body, Encoding.UTF8); + var body = await reader.ReadToEndAsync(); + var resource = _parser.Parse(body); + context.HttpContext.AddResourceType(resource.GetType()); + + return await InputFormatterResult.SuccessAsync(resource); + } + catch (FormatException exception) + { + throw Error.BadRequest($"Body parsing failed: {exception.Message}"); + } + } + } +} +#endif \ No newline at end of file diff --git a/src/Spark.Engine/Formatters/NetCore/AsyncResourceJsonOutputFormatter.cs b/src/Spark.Engine/Formatters/NetCore/AsyncResourceJsonOutputFormatter.cs new file mode 100644 index 000000000..07e992dbc --- /dev/null +++ b/src/Spark.Engine/Formatters/NetCore/AsyncResourceJsonOutputFormatter.cs @@ -0,0 +1,95 @@ +#if NETSTANDARD2_0 +using FhirModel = Hl7.Fhir.Model; +using Hl7.Fhir.Rest; +using Hl7.Fhir.Serialization; +using Microsoft.AspNetCore.Mvc.Formatters; +using Newtonsoft.Json; +using Spark.Core; +using Spark.Engine.Core; +using Spark.Engine.Extensions; +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using System.Net; + +namespace Spark.Engine.Formatters +{ + public class AsyncResourceJsonOutputFormatter : TextOutputFormatter + { + public static readonly string[] JsonMediaTypes = + { + "application/json", + "application/fhir+json", + "application/json+fhir", + "text/json" + }; + + public AsyncResourceJsonOutputFormatter() + { + SupportedEncodings.Clear(); + SupportedEncodings.Add(Encoding.UTF8); + + foreach (var mediaType in JsonMediaTypes) + { + SupportedMediaTypes.Add(mediaType); + } + } + + protected override bool CanWriteType(Type type) + { + return typeof(FhirModel.Resource).IsAssignableFrom(type) + || typeof(FhirResponse).IsAssignableFrom(type) + || typeof(ValidationProblemDetails).IsAssignableFrom(type); + } + + public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding) + { + if (context == null) throw new ArgumentNullException(nameof(context)); + if (selectedEncoding == null) throw new ArgumentNullException(nameof(selectedEncoding)); + if (selectedEncoding != Encoding.UTF8) throw Error.BadRequest($"FHIR supports UTF-8 encoding exclusively, not {selectedEncoding.WebName}"); + + if (!(context.HttpContext.RequestServices.GetService(typeof(FhirJsonSerializer)) is FhirJsonSerializer serializer)) + throw Error.Internal($"Missing required dependency '{nameof(FhirJsonSerializer)}'"); + + var responseBody = context.HttpContext.Response.Body; + var writeBodyString = string.Empty; + var summaryType = context.HttpContext.Request.RequestSummary(); + + if (typeof(FhirResponse).IsAssignableFrom(context.ObjectType)) + { + FhirResponse response = context.Object as FhirResponse; + + context.HttpContext.Response.AcquireHeaders(response); + context.HttpContext.Response.StatusCode = (int)response.StatusCode; + + if (response.Resource != null) + { + writeBodyString = serializer.SerializeToString(response.Resource, summaryType); + } + } + else if (context.ObjectType == typeof(FhirModel.OperationOutcome) || typeof(FhirModel.Resource).IsAssignableFrom(context.ObjectType)) + { + if (context.Object != null) + { + writeBodyString = serializer.SerializeToString(context.Object as FhirModel.Resource, summaryType); + } + } + else if (context.Object is ValidationProblemDetails validationProblems) + { + FhirModel.OperationOutcome outcome = new FhirModel.OperationOutcome(); + outcome.AddValidationProblems(context.HttpContext.GetResourceType(), (HttpStatusCode)context.HttpContext.Response.StatusCode, validationProblems); + writeBodyString = serializer.SerializeToString(outcome, summaryType); + } + + if (!string.IsNullOrWhiteSpace(writeBodyString)) + { + var writeBuffer = selectedEncoding.GetBytes(writeBodyString); + await responseBody.WriteAsync(writeBuffer, 0, writeBuffer.Length); + await responseBody.FlushAsync(); + } + } + } +} +#endif \ No newline at end of file diff --git a/src/Spark.Engine/Formatters/NetCore/AsyncResourceXmlInputFormatter.cs b/src/Spark.Engine/Formatters/NetCore/AsyncResourceXmlInputFormatter.cs new file mode 100644 index 000000000..1cd691ddb --- /dev/null +++ b/src/Spark.Engine/Formatters/NetCore/AsyncResourceXmlInputFormatter.cs @@ -0,0 +1,65 @@ +#if NETSTANDARD2_0 +using Hl7.Fhir.Model; +using Hl7.Fhir.Serialization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.AspNetCore.WebUtilities; +using Spark.Core; +using Spark.Engine.Extensions; +using Spark.Engine.IO; +using System; +using System.Diagnostics; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using System.Xml; + +namespace Spark.Engine.Formatters +{ + public class AsyncResourceXmlInputFormatter : TextInputFormatter + { + private readonly FhirXmlParser _parser; + + public AsyncResourceXmlInputFormatter(FhirXmlParser parser) + { + _parser = parser; + + SupportedEncodings.Clear(); + SupportedEncodings.Add(Encoding.UTF8); + + SupportedMediaTypes.Add("application/xml"); + SupportedMediaTypes.Add("application/fhir+xml"); + SupportedMediaTypes.Add("application/xml+fhir"); + SupportedMediaTypes.Add("text/xml"); + SupportedMediaTypes.Add("text/xml+fhir"); + } + + protected override bool CanReadType(Type type) + { + return typeof(Resource).IsAssignableFrom(type); + } + + public override async Task ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding) + { + if (context == null) throw new ArgumentNullException(nameof(context)); + if (encoding == null) throw new ArgumentNullException(nameof(encoding)); + if (encoding != Encoding.UTF8) + throw Error.BadRequest("FHIR supports UTF-8 encoding exclusively, not " + encoding.WebName); + + try + { + using var reader = new StreamReader(context.HttpContext.Request.Body, Encoding.UTF8); + var body = await reader.ReadToEndAsync(); + var resource = _parser.Parse(body); + context.HttpContext.AddResourceType(resource.GetType()); + + return await InputFormatterResult.SuccessAsync(resource); + } + catch (FormatException exception) + { + throw Error.BadRequest($"Body parsing failed: {exception.Message}"); + } + } + } +} +#endif \ No newline at end of file diff --git a/src/Spark.Engine/Formatters/NetCore/AsyncResourceXmlOutputFormatter.cs b/src/Spark.Engine/Formatters/NetCore/AsyncResourceXmlOutputFormatter.cs new file mode 100644 index 000000000..6986b6429 --- /dev/null +++ b/src/Spark.Engine/Formatters/NetCore/AsyncResourceXmlOutputFormatter.cs @@ -0,0 +1,97 @@ +#if NETSTANDARD2_0 +using FhirModel = Hl7.Fhir.Model; +using Hl7.Fhir.Rest; +using Hl7.Fhir.Serialization; +using Microsoft.AspNetCore.Mvc.Formatters; +using Spark.Core; +using Spark.Engine.Core; +using Spark.Engine.Extensions; +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using System.Xml; +using Microsoft.AspNetCore.Mvc; +using System.Net; + +namespace Spark.Engine.Formatters +{ + public class AsyncResourceXmlOutputFormatter : TextOutputFormatter + { + public static readonly string[] XmlMediaTypes = + { + "application/xml", + "application/fhir+xml", + "application/xml+fhir", + "text/xml", + "text/xml+fhir" + }; + + public AsyncResourceXmlOutputFormatter() + { + SupportedEncodings.Clear(); + SupportedEncodings.Add(Encoding.UTF8); + + foreach (var mediaType in XmlMediaTypes) + { + SupportedMediaTypes.Add(mediaType); + } + } + + protected override bool CanWriteType(Type type) + { + return + typeof(FhirModel.Resource).IsAssignableFrom(type) + || typeof(FhirResponse).IsAssignableFrom(type) + || typeof(ValidationProblemDetails).IsAssignableFrom(type); + } + + public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding) + { + if (context == null) throw new ArgumentNullException(nameof(context)); + if (selectedEncoding == null) throw new ArgumentNullException(nameof(selectedEncoding)); + if (selectedEncoding != Encoding.UTF8) throw Error.BadRequest($"FHIR supports UTF-8 encoding exclusively, not {selectedEncoding.WebName}"); + + if (!(context.HttpContext.RequestServices.GetService(typeof(FhirXmlSerializer)) is FhirXmlSerializer serializer)) + throw Error.Internal($"Missing required dependency '{nameof(FhirXmlSerializer)}'"); + + var responseBody = context.HttpContext.Response.Body; + var writeBodyString = string.Empty; + var summaryType = context.HttpContext.Request.RequestSummary(); + + if (typeof(FhirResponse).IsAssignableFrom(context.ObjectType)) + { + FhirResponse response = context.Object as FhirResponse; + + context.HttpContext.Response.AcquireHeaders(response); + context.HttpContext.Response.StatusCode = (int)response.StatusCode; + + if (response.Resource != null) + { + writeBodyString = serializer.SerializeToString(response.Resource, summaryType); + } + } + else if (context.ObjectType == typeof(FhirModel.OperationOutcome) || typeof(FhirModel.Resource).IsAssignableFrom(context.ObjectType)) + { + if (context.Object != null) + { + writeBodyString = serializer.SerializeToString(context.Object as FhirModel.Resource, summaryType); + } + } + else if (context.Object is ValidationProblemDetails validationProblems) + { + FhirModel.OperationOutcome outcome = new FhirModel.OperationOutcome(); + outcome.AddValidationProblems(context.HttpContext.GetResourceType(), (HttpStatusCode)context.HttpContext.Response.StatusCode, validationProblems); + writeBodyString = serializer.SerializeToString(outcome, summaryType); + } + + if (!string.IsNullOrWhiteSpace(writeBodyString)) + { + var writeBuffer = selectedEncoding.GetBytes(writeBodyString); + await responseBody.WriteAsync(writeBuffer, 0, writeBuffer.Length); + await responseBody.FlushAsync(); + } + } + } +} +#endif diff --git a/src/Spark.Engine/SparkSettings.cs b/src/Spark.Engine/SparkSettings.cs index 45ac398d3..31f66d318 100644 --- a/src/Spark.Engine/SparkSettings.cs +++ b/src/Spark.Engine/SparkSettings.cs @@ -9,6 +9,7 @@ namespace Spark.Engine public class SparkSettings { public Uri Endpoint { get; set; } + public bool UseAsynchronousIO { get; set; } public ParserSettings ParserSettings { get; set; } public SerializerSettings SerializerSettings { get; set; } public ExportSettings ExportSettings { get; set; }