diff --git a/src/Hl7.Fhir.Base/CompatibilitySuppressions.xml b/src/Hl7.Fhir.Base/CompatibilitySuppressions.xml index f79ce1c7ea..beb25c62eb 100644 --- a/src/Hl7.Fhir.Base/CompatibilitySuppressions.xml +++ b/src/Hl7.Fhir.Base/CompatibilitySuppressions.xml @@ -185,49 +185,91 @@ CP0002 - M:Hl7.Fhir.Serialization.IFhirSerializationEngine.SerializeToXml(Hl7.Fhir.Model.Resource) + M:Hl7.Fhir.Serialization.BaseFhirJsonPocoSerializer.Serialize(System.Collections.Generic.IReadOnlyDictionary{System.String,System.Object},System.Text.Json.Utf8JsonWriter) lib/net8.0/Hl7.Fhir.Base.dll lib/net8.0/Hl7.Fhir.Base.dll true CP0002 - M:Hl7.Fhir.Serialization.SerializationEngineExtensions.SerializeReaderToXml(Hl7.Fhir.Serialization.IFhirSerializationEngine,System.Xml.XmlReader) + M:Hl7.Fhir.Serialization.BaseFhirJsonPocoSerializer.SerializeToString(System.Collections.Generic.IReadOnlyDictionary{System.String,System.Object}) lib/net8.0/Hl7.Fhir.Base.dll lib/net8.0/Hl7.Fhir.Base.dll true CP0002 - M:Hl7.Fhir.Serialization.SerializationEngineExtensions.SerializeToXmlWriter(Hl7.Fhir.Serialization.IFhirSerializationEngine,Hl7.Fhir.Model.Resource,System.Xml.XmlWriter) + M:Hl7.Fhir.Serialization.BaseFhirXmlPocoSerializer.Serialize(System.Collections.Generic.IReadOnlyDictionary{System.String,System.Object},System.Xml.XmlWriter,Hl7.Fhir.Serialization.SerializationFilter) lib/net8.0/Hl7.Fhir.Base.dll lib/net8.0/Hl7.Fhir.Base.dll true CP0002 - M:Hl7.Fhir.Utility.ReflectionHelper.IsTypedCollection(System.Type) + M:Hl7.Fhir.Serialization.BaseFhirXmlPocoSerializer.SerializeToString(System.Collections.Generic.IReadOnlyDictionary{System.String,System.Object},Hl7.Fhir.Serialization.SerializationFilter) lib/net8.0/Hl7.Fhir.Base.dll lib/net8.0/Hl7.Fhir.Base.dll true CP0002 - M:Hl7.FhirPath.EvaluationContext.WithResourceOverrides(Hl7.Fhir.ElementModel.ITypedElement,Hl7.Fhir.ElementModel.ITypedElement) + M:Hl7.Fhir.Serialization.FhirJsonConverter`1.#ctor(Hl7.Fhir.Introspection.ModelInspector) lib/net8.0/Hl7.Fhir.Base.dll lib/net8.0/Hl7.Fhir.Base.dll true - CP0006 - M:Hl7.Fhir.ElementModel.ITypedElement.Children(System.String) + CP0002 + M:Hl7.Fhir.Serialization.FhirJsonConverter`1.#ctor(Hl7.Fhir.Serialization.BaseFhirJsonPocoDeserializer,Hl7.Fhir.Serialization.BaseFhirJsonPocoSerializer) + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + + + CP0002 + M:Hl7.Fhir.Serialization.FhirJsonConverter`1.#ctor(System.Reflection.Assembly,Hl7.Fhir.Serialization.FhirJsonPocoSerializerSettings,Hl7.Fhir.Serialization.FhirJsonPocoDeserializerSettings,System.Predicate{Hl7.Fhir.Utility.CodedException}) + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + + + CP0002 + M:Hl7.Fhir.Serialization.FhirJsonConverter`1.#ctor(System.Reflection.Assembly) + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + + + CP0002 + M:Hl7.Fhir.Serialization.SerializationEngineExtensions.SerializeReaderToJson(Hl7.Fhir.Serialization.IFhirSerializationEngine,System.Text.Json.Utf8JsonReader@) + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + + + CP0002 + M:Hl7.Fhir.Serialization.SerializationEngineExtensions.SerializeReaderToXml(Hl7.Fhir.Serialization.IFhirSerializationEngine,System.Xml.XmlReader) + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + + + CP0002 + M:Hl7.Fhir.Utility.ReflectionHelper.IsTypedCollection(System.Type) + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + + + CP0002 + M:Hl7.FhirPath.EvaluationContext.WithResourceOverrides(Hl7.Fhir.ElementModel.ITypedElement,Hl7.Fhir.ElementModel.ITypedElement) lib/net8.0/Hl7.Fhir.Base.dll lib/net8.0/Hl7.Fhir.Base.dll true CP0006 - M:Hl7.Fhir.Serialization.IFhirSerializationEngine.SerializeToXml(Hl7.Fhir.Model.Base) + M:Hl7.Fhir.ElementModel.ITypedElement.Children(System.String) lib/net8.0/Hl7.Fhir.Base.dll lib/net8.0/Hl7.Fhir.Base.dll true @@ -288,6 +330,13 @@ lib/net8.0/Hl7.Fhir.Base.dll true + + CP0009 + T:Hl7.Fhir.Serialization.FhirJsonConverter`1 + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + PKV006 .NETStandard,Version=v2.0 diff --git a/src/Hl7.Fhir.Base/Model/Base.Dictionary.cs b/src/Hl7.Fhir.Base/Model/Base.Dictionary.cs index 50703025f5..5c55ebe274 100644 --- a/src/Hl7.Fhir.Base/Model/Base.Dictionary.cs +++ b/src/Hl7.Fhir.Base/Model/Base.Dictionary.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Linq; +#nullable enable + namespace Hl7.Fhir.Model; public abstract partial class Base: IReadOnlyDictionary, IDictionary diff --git a/src/Hl7.Fhir.Base/Serialization/BaseFhirJsonPocoSerializer.cs b/src/Hl7.Fhir.Base/Serialization/BaseFhirJsonPocoSerializer.cs index cdbd553228..caedb9f664 100644 --- a/src/Hl7.Fhir.Base/Serialization/BaseFhirJsonPocoSerializer.cs +++ b/src/Hl7.Fhir.Base/Serialization/BaseFhirJsonPocoSerializer.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) 2021, Firely (info@fire.ly) and contributors * See the file CONTRIBUTORS for details. * @@ -20,314 +20,310 @@ using System.Text; using System.Text.Json; -namespace Hl7.Fhir.Serialization +namespace Hl7.Fhir.Serialization; + +/// +/// Serializes the contents of an IReadOnlyDictionary[string,object] according to the rules of FHIR Json serialization. +/// +/// The serializer uses the format documented in https://www.hl7.org/fhir/json.html. Since all POCOs included +/// in the SDK implement IReadOnlyDictionary, these methods can be used to serialize POCOs to Json. +/// +public class BaseFhirJsonPocoSerializer { /// - /// Serializes the contents of an IReadOnlyDictionary[string,object] according to the rules of FHIR Json serialization. + /// Construct a new serializer for a specific release of FHIR. /// - /// The serializer uses the format documented in https://www.hl7.org/fhir/json.html. Since all POCOs included - /// in the SDK implement IReadOnlyDictionary, these methods can be used to serialize POCOs to Json. - /// - public class BaseFhirJsonPocoSerializer + public BaseFhirJsonPocoSerializer(FhirRelease release) : this(release, new()) { - /// - /// Construct a new serializer for a specific release of FHIR. - /// - public BaseFhirJsonPocoSerializer(FhirRelease release) : this(release, new()) - { - // nothing - } + // nothing + } - /// - /// Construct a new serializer for a specific release of FHIR. - /// - public BaseFhirJsonPocoSerializer(FhirRelease release, FhirJsonPocoSerializerSettings settings) - { - Release = release; - Settings = settings; - } + /// + /// Construct a new serializer for a specific release of FHIR. + /// + public BaseFhirJsonPocoSerializer(FhirRelease release, FhirJsonPocoSerializerSettings settings) + { + Release = release; + Settings = settings; + } - /// - /// The release of FHIR for which this serializer is configured. - /// - public FhirRelease Release { get; } - - /// - /// The settings that were passed to the constructor. - /// - public FhirJsonPocoSerializerSettings Settings { get; } - - /// - /// Serializes the given dictionary with FHIR data into Json. - /// - public void Serialize(IReadOnlyDictionary members, Utf8JsonWriter writer) => - serializeInternal(members, writer, skipValue: false); - - /// - /// Serializes the given dictionary with FHIR data into a Json string. - /// - public string SerializeToString(IReadOnlyDictionary members) - { - var stream = new MemoryStream(); - var writer = new Utf8JsonWriter(stream); - serializeInternal(members, writer, skipValue: false); - writer.Flush(); - return Encoding.UTF8.GetString(stream.ToArray()); - } + /// + /// The release of FHIR for which this serializer is configured. + /// + public FhirRelease Release { get; } - /// - /// Serializes the given dictionary with FHIR data into Json, optionally skipping the "value" element. - /// - /// Not serializing the "value" element is useful when serializing FHIR primitives into two properties, one - /// with just the value, and one with the id/extensions. - private void serializeInternal( - IReadOnlyDictionary members, - Utf8JsonWriter writer, - bool skipValue) - { - writer.WriteStartObject(); - var filter = Settings.SummaryFilter; + /// + /// The settings that were passed to the constructor. + /// + public FhirJsonPocoSerializerSettings Settings { get; } + + /// + /// Serializes the given dictionary with FHIR data into Json. + /// + public void Serialize(Base element, Utf8JsonWriter writer) => + serializeInternal(element, writer, skipValue: false); + + /// + /// Serializes the given dictionary with FHIR data into a Json string. + /// + public string SerializeToString(Base element) + { + var stream = new MemoryStream(); + var writer = new Utf8JsonWriter(stream); + serializeInternal(element, writer, skipValue: false); + writer.Flush(); + return Encoding.UTF8.GetString(stream.ToArray()); + } + + /// + /// Serializes the given dictionary with FHIR data into Json, optionally skipping the "value" element. + /// + /// Not serializing the "value" element is useful when serializing FHIR primitives into two properties, one + /// with just the value, and one with the id/extensions. + private void serializeInternal( + Base element, + Utf8JsonWriter writer, + bool skipValue) + { + writer.WriteStartObject(); + var filter = Settings.SummaryFilter; - if (members is Resource r) - writer.WriteString("resourceType", r.TypeName); + if (element is Resource r) + writer.WriteString("resourceType", r.TypeName); - // Only throw if we don't have a mapping where we are expected to: when this is a subclass of Base. - if (!ClassMapping.TryGetMappingForType(members.GetType(), Release, out var mapping) && members is Base) - throw new InvalidOperationException($"Encountered type {members.GetType()}, which is a support POCO for FHIR, but does not " + - $"have sufficient metadata to be used by the serializer."); + // Only throw if we don't have a mapping where we are expected to: when this is a subclass of Base. + if (!ClassMapping.TryGetMappingForType(element.GetType(), Release, out var mapping)) + throw new InvalidOperationException($"Encountered type {element.GetType()}, which is a support POCO for FHIR, but does not " + + $"have sufficient metadata to be used by the serializer."); - filter?.EnterObject(members, mapping); + filter?.EnterObject(element, mapping); - foreach (var member in members) - { - if (skipValue && member.Key == "value") continue; + foreach (var member in element) + { + if (skipValue && member.Key == "value") continue; - var propertyMapping = mapping?.FindMappedElementByName(member.Key); + var propertyMapping = mapping?.FindMappedElementByName(member.Key); - if (filter?.TryEnterMember(member.Key, member.Value, propertyMapping) == false) - continue; + if (filter?.TryEnterMember(member.Key, member.Value, propertyMapping) == false) + continue; - var propertyName = propertyMapping?.Choice == ChoiceType.DatatypeChoice ? - addSuffixToElementName(member.Key, member.Value) : member.Key; + var propertyName = propertyMapping?.Choice == ChoiceType.DatatypeChoice ? + addSuffixToElementName(member.Key, member.Value) : member.Key; - // do we have more possible types (choiceType), then the required type is dependent on the type of the member.Value (is it Integer64 or not?), - // otherwise we will use the Fhirtype of the propertyMapping. - var requiredType = (propertyMapping?.FhirType.Length > 1) - ? (member.Value is Integer64 ? typeof(Integer64) : null) - : propertyMapping?.FhirType.FirstOrDefault(); + // do we have more possible types (choiceType), then the required type is dependent on the type of the member.Value (is it Integer64 or not?), + // otherwise we will use the Fhirtype of the propertyMapping. + var requiredType = (propertyMapping?.FhirType.Length > 1) + ? (member.Value is Integer64 ? typeof(Integer64) : null) + : propertyMapping?.FhirType.FirstOrDefault(); - if (member.Value is PrimitiveType pt) - serializeFhirPrimitive(propertyName, pt, writer, requiredType); - else if (member.Value is IReadOnlyCollection pts) - serializeFhirPrimitiveList(propertyName, pts, writer, requiredType); - else - { - writer.WritePropertyName(propertyName); + if (member.Value is PrimitiveType pt) + serializeFhirPrimitive(propertyName, pt, writer, requiredType); + else if (member.Value is IReadOnlyCollection pts) + serializeFhirPrimitiveList(propertyName, pts, writer, requiredType); + else + { + writer.WritePropertyName(propertyName); - if (member.Value is ICollection coll && member.Value is not byte[]) - { - writer.WriteStartArray(); + if (member.Value is ICollection coll and not byte[]) + { + writer.WriteStartArray(); - foreach (var value in coll) - serializeMemberValue(value, writer, requiredType); + foreach (var value in coll) + serializeMemberValue(value, writer, requiredType); - writer.WriteEndArray(); - } - else - serializeMemberValue(member.Value, writer, requiredType); + writer.WriteEndArray(); } - - filter?.LeaveMember(member.Key, member.Value, propertyMapping); + else + serializeMemberValue(member.Value, writer, requiredType); } - filter?.LeaveObject(members, mapping); - writer.WriteEndObject(); + filter?.LeaveMember(member.Key, member.Value, propertyMapping); } - private static string addSuffixToElementName(string elementName, object elementValue) + filter?.LeaveObject(element, mapping); + writer.WriteEndObject(); + } + + private static string addSuffixToElementName(string elementName, object elementValue) + { + var typeName = elementValue switch { - var typeName = elementValue switch - { - IEnumerable ib => ib.FirstOrDefault()?.TypeName, - Base b => b.TypeName, - _ => null - }; + IEnumerable ib => ib.FirstOrDefault()?.TypeName, + Base b => b.TypeName, + _ => null + }; - return typeName is null ? elementName : elementName + char.ToUpperInvariant(typeName[0]) + typeName.Substring(1); - } + return typeName is null ? elementName : elementName + char.ToUpperInvariant(typeName[0]) + typeName.Substring(1); + } - private void serializeMemberValue(object value, Utf8JsonWriter writer, Type? requiredType = null) - { - if (value is IReadOnlyDictionary complex) - serializeInternal(complex, writer, skipValue: false); - else - SerializePrimitiveValue(value, writer, requiredType); - } + private void serializeMemberValue(object value, Utf8JsonWriter writer, Type? requiredType = null) + { + if (value is Base complex) + serializeInternal(complex, writer, skipValue: false); + else + SerializePrimitiveValue(value, writer, requiredType); + } - /// - /// Serializes a list of FHIR primitives into an array element with the given name - /// - /// FHIR primitives are handled separately here since they may require - /// serialization into two Json properties called "elementName" and "_elementName" and - /// may use Json nulls as placeholders. - private void serializeFhirPrimitiveList( - string elementName, - IReadOnlyCollection values, - Utf8JsonWriter writer, - Type? requiredType = null) - { - if (values is null) throw new ArgumentNullException(nameof(values)); + /// + /// Serializes a list of FHIR primitives into an array element with the given name + /// + /// FHIR primitives are handled separately here since they may require + /// serialization into two Json properties called "elementName" and "_elementName" and + /// may use Json nulls as placeholders. + private void serializeFhirPrimitiveList( + string elementName, + IReadOnlyCollection values, + Utf8JsonWriter writer, + Type? requiredType = null) + { + if(values is null) throw new ArgumentNullException(nameof(values)); - // Don't serialize empty collections. - if (values.Count == 0) return; + // Don't serialize empty collections. + if (values.Count == 0) return; - // We should not write a "elementName" property until we encounter an actual - // value. If we do, we should "catch up", by creating the property starting - // with a json array that contains 'null' for each of the elements we encountered - // until now that did not have a value id/extensions. - bool wroteStartArray = false; - int numNullsMissed = 0; + // We should not write a "elementName" property until we encounter an actual + // value. If we do, we should "catch up", by creating the property starting + // with a json array that contains 'null' for each of the elements we encountered + // until now that did not have a value id/extensions. + bool wroteStartArray = false; + int numNullsMissed = 0; - foreach (var value in values) + foreach (var value in values) + { + if (value?.ObjectValue is not null) { - if (value?.ObjectValue is not null) + if (!wroteStartArray) { - if (!wroteStartArray) - { - wroteStartArray = true; - writeStartArray(elementName, numNullsMissed, writer); - } - - SerializePrimitiveValue(value!.ObjectValue, writer, requiredType); + wroteStartArray = true; + writeStartArray(elementName, numNullsMissed, writer); } + + SerializePrimitiveValue(value!.ObjectValue, writer, requiredType); + } + else + { + if (wroteStartArray) + writer.WriteNullValue(); else - { - if (wroteStartArray) - writer.WriteNullValue(); - else - numNullsMissed += 1; - } + numNullsMissed += 1; } + } - if (wroteStartArray) writer.WriteEndArray(); + if (wroteStartArray) writer.WriteEndArray(); - // We should not write a "_elementName" property until we encounter an actual - // id/extension. If we do, we should "catch up", by creating the property starting - // with a json array that contains 'null' for each of the elements we encountered - // until now that did not have id/extensions etc. - wroteStartArray = false; - numNullsMissed = 0; + // We should not write a "_elementName" property until we encounter an actual + // id/extension. If we do, we should "catch up", by creating the property starting + // with a json array that contains 'null' for each of the elements we encountered + // until now that did not have id/extensions etc. + wroteStartArray = false; + numNullsMissed = 0; - foreach (var value in values) + foreach (var value in values) + { + if (value?.HasElements == true) { - if (value?.HasElements == true) + if (!wroteStartArray) { - if (!wroteStartArray) - { - wroteStartArray = true; - writeStartArray("_" + elementName, numNullsMissed, writer); - } - - serializeInternal(value, writer, skipValue: true); + wroteStartArray = true; + writeStartArray("_" + elementName, numNullsMissed, writer); } + + serializeInternal(value, writer, skipValue: true); + } + else + { + if (wroteStartArray) + writer.WriteNullValue(); else - { - if (wroteStartArray) - writer.WriteNullValue(); - else - numNullsMissed += 1; - } + numNullsMissed += 1; } - - if (wroteStartArray) writer.WriteEndArray(); } - private static void writeStartArray(string propName, int numNulls, Utf8JsonWriter writer) - { - writer.WriteStartArray(propName); + if (wroteStartArray) writer.WriteEndArray(); + } - for (int i = 0; i < numNulls; i++) - writer.WriteNullValue(); - } + private static void writeStartArray(string propName, int numNulls, Utf8JsonWriter writer) + { + writer.WriteStartArray(propName); + for (int i = 0; i < numNulls; i++) + writer.WriteNullValue(); + } - /// - /// Serializes a FHIR primitive into an element with the given name - /// - /// FHIR primitives are handled separately here since they may require - /// serialization into two Json properties called "elementName" and "_elementName". - private void serializeFhirPrimitive(string elementName, PrimitiveType value, Utf8JsonWriter writer, Type? requiredType = null) - { - if (value is null) throw new ArgumentNullException(nameof(value)); - if (value.ObjectValue is not null) - { - // Write a property with 'elementName' - writer.WritePropertyName(elementName); - SerializePrimitiveValue(value.ObjectValue, writer, requiredType); - } + /// + /// Serializes a FHIR primitive into an element with the given name + /// + /// FHIR primitives are handled separately here since they may require + /// serialization into two Json properties called "elementName" and "_elementName". + private void serializeFhirPrimitive(string elementName, PrimitiveType value, Utf8JsonWriter writer, Type? requiredType = null) + { + if (value is null) throw new ArgumentNullException(nameof(value)); - if (value.HasElements) - { - // Write a property with '_elementName' - writer.WritePropertyName("_" + elementName); - serializeInternal(value, writer, skipValue: true); - } + if (value.ObjectValue is not null) + { + // Write a property with 'elementName' + writer.WritePropertyName(elementName); + SerializePrimitiveValue(value.ObjectValue, writer, requiredType); } - /// - /// Serialize a primitive .NET value that may occur in the POCOs into Json. - /// - /// - /// To allow for future additions to the POCOs the list of primitives supported here - /// is larger than the set used by the current POCOs. Note that DateTimeOffsetc> and - /// byte[] are considered to be "primitive" values here (used as the value in - /// and ). - /// - /// Note that the current version of System.Text.Json only allows numbers - /// to be written that fit in .NET's type, which may be less - /// precision than required by the FHIR specification (http://hl7.org/fhir/json.html#primitive). - /// - protected virtual void SerializePrimitiveValue(object value, Utf8JsonWriter writer, Type? requiredType) + if (value.HasElements) { - switch (value) - { - case int i32: writer.WriteNumberValue(i32); break; - case uint ui32: writer.WriteNumberValue(ui32); break; - case long i64: - { - // in case of Integer64, then the value must be serialized as a string due to - // issues with precision in floating point libraries. - if (requiredType == typeof(Integer64)) - writer.WriteStringValue(i64.ToString()); - else - writer.WriteNumberValue(i64); - break; - } - case ulong ui64: writer.WriteNumberValue(ui64); break; - case float si: writer.WriteNumberValue(si); break; - case double dbl: writer.WriteNumberValue(dbl); break; - case decimal dec: writer.WriteNumberValue(dec); break; - // A little note about trimming and whitespaces. The spec says: - // "(...) In JSON and Turtle whitespace in string values is always significant. Primitive types other than - // string SHALL NOT have leading or trailing whitespace." - // Based on this, we are not trimming whitespace here. Validation is not a part of the responsibilities of - // the serializer, and string-based types (like code and uri) should make sure their values are valid, - // so should not have trailing spaces to begin with. strings are allowed to have trailing spaces, but should - // not just be spaces. The serializer will, however, not serialize an element with only whitespace - // (or an empty byte[]). - case string s: writer.WriteStringValue(s); break; - case bool b: writer.WriteBooleanValue(b); break; - case DateTimeOffset dto: writer.WriteStringValue(ElementModel.Types.DateTime.FormatDateTimeOffset(dto)); break; - case Enum e: writer.WriteStringValue(e.GetLiteral()); break; - case byte[] bytes: writer.WriteStringValue(Convert.ToBase64String(bytes)); break; - case null: writer.WriteNullValue(); break; - default: - throw new FormatException($"There is no known serialization for type {value.GetType()} into a Json primitive property value."); - } + // Write a property with '_elementName' + writer.WritePropertyName("_" + elementName); + serializeInternal(value, writer, skipValue: true); } } -} - -#nullable restore + /// + /// Serialize a primitive .NET value that may occur in the POCOs into Json. + /// + /// + /// To allow for future additions to the POCOs the list of primitives supported here + /// is larger than the set used by the current POCOs. Note that DateTimeOffsetc> and + /// byte[] are considered to be "primitive" values here (used as the value in + /// and ). + /// + /// Note that the current version of System.Text.Json only allows numbers + /// to be written that fit in .NET's type, which may be less + /// precision than required by the FHIR specification (http://hl7.org/fhir/json.html#primitive). + /// + protected virtual void SerializePrimitiveValue(object value, Utf8JsonWriter writer, Type? requiredType) + { + switch (value) + { + case int i32: writer.WriteNumberValue(i32); break; + case uint ui32: writer.WriteNumberValue(ui32); break; + case long i64: + { + // in case of Integer64, then the value must be serialized as a string due to + // issues with precision in floating point libraries. + if (requiredType == typeof(Integer64)) + writer.WriteStringValue(i64.ToString()); + else + writer.WriteNumberValue(i64); + break; + } + case ulong ui64: writer.WriteNumberValue(ui64); break; + case float si: writer.WriteNumberValue(si); break; + case double dbl: writer.WriteNumberValue(dbl); break; + case decimal dec: writer.WriteNumberValue(dec); break; + // A little note about trimming and whitespaces. The spec says: + // "(...) In JSON and Turtle whitespace in string values is always significant. Primitive types other than + // string SHALL NOT have leading or trailing whitespace." + // Based on this, we are not trimming whitespace here. Validation is not a part of the responsibilities of + // the serializer, and string-based types (like code and uri) should make sure their values are valid, + // so should not have trailing spaces to begin with. strings are allowed to have trailing spaces, but should + // not just be spaces. The serializer will, however, not serialize an element with only whitespace + // (or an empty byte[]). + case string s: writer.WriteStringValue(s); break; + case bool b: writer.WriteBooleanValue(b); break; + case DateTimeOffset dto: writer.WriteStringValue(ElementModel.Types.DateTime.FormatDateTimeOffset(dto)); break; + case Enum e: writer.WriteStringValue(e.GetLiteral()); break; + case byte[] bytes: writer.WriteStringValue(Convert.ToBase64String(bytes)); break; + case null: writer.WriteNullValue(); break; + default: + throw new FormatException($"There is no known serialization for type {value.GetType()} into a Json primitive property value."); + } + } +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Serialization/BaseFhirXmlPocoSerializer.cs b/src/Hl7.Fhir.Base/Serialization/BaseFhirXmlPocoSerializer.cs index 86490a6442..37b4091342 100644 --- a/src/Hl7.Fhir.Base/Serialization/BaseFhirXmlPocoSerializer.cs +++ b/src/Hl7.Fhir.Base/Serialization/BaseFhirXmlPocoSerializer.cs @@ -19,188 +19,178 @@ using System.Linq; using System.Xml; -namespace Hl7.Fhir.Serialization +namespace Hl7.Fhir.Serialization; + +/// +/// Serializes the contents of a POCO according to the rules of FHIR Xml serialization. +/// +/// The serializer uses the format documented in https://www.hl7.org/fhir/xml.html. +/// +public class BaseFhirXmlPocoSerializer { /// - /// Serializes the contents of an IReadOnlyDictionary[string,object] according to the rules of FHIR Xml serialization. + /// The release of FHIR for which this serializer is configured. /// - /// The serializer uses the format documented in https://www.hl7.org/fhir/xml.html. Since all POCOs included - /// in the SDK implement IReadOnlyDictionary, these methods can be used to serialize POCOs to Xml. - /// - public class BaseFhirXmlPocoSerializer + public FhirRelease Release { get; } + + /// + /// Construct a new serializer for a specific release of FHIR. + /// + public BaseFhirXmlPocoSerializer(FhirRelease release) { - /// - /// The release of FHIR for which this serializer is configured. - /// - public FhirRelease Release { get; } - - /// - /// Construct a new serializer for a specific release of FHIR. - /// - public BaseFhirXmlPocoSerializer(FhirRelease release) - { - Release = release; - } + Release = release; + } - /// - /// Serializes the given dictionary with FHIR data into Json. - /// - public void Serialize(IReadOnlyDictionary members, XmlWriter writer, SerializationFilter? summary = default) + /// + /// Serializes the given dictionary with FHIR data into Json. + /// + public void Serialize( + Base element, + XmlWriter writer, + SerializationFilter? summary = default) + { + writer.WriteStartDocument(); + + // If we are serializing a non-resource, or we are serializing a nested resource, + // we need to pick a name for the root element. + var pickElementName = element is not Resource or IScopedNode { Parent: not null }; + if (pickElementName) { - writer.WriteStartDocument(); + // If we are an element with a name, pick that, otherwise us the name of the type. + var nodeName = element is ITypedElement ite ? ite.Name : element.TypeName; - var simulateRoot = ((IScopedNode)members).Parent is not null || members is not Resource; - if (simulateRoot) - { - // Serialization in XML of non-resources is problematic, since there's no root. - // It's a common usecase though, so "invent" a root that's the name of the element's type. - var rootElementName = members is Base b ? ((ITypedElement)b).Name : members.GetType().Name; - writer.WriteStartElement(rootElementName, XmlNs.FHIR); - } + writer.WriteStartElement(nodeName, XmlNs.FHIR); + } - serializeInternal(members, writer, summary); + serializeInternal(element, writer, summary); - if (simulateRoot) writer.WriteEndElement(); - writer.WriteEndDocument(); - } + if (pickElementName) writer.WriteEndElement(); + writer.WriteEndDocument(); + } - /// - /// Serializes the given dictionary with FHIR data into UTF8 encoded Json. - /// - public string SerializeToString(IReadOnlyDictionary members, SerializationFilter? summary = default) => - SerializationUtil.WriteXmlToString(w => Serialize(members, w, summary)); - - /// - /// Serializes the given dictionary with FHIR data into Json, optionally skipping the "value" element. - /// - /// Not serializing the "value" element is useful when serializing FHIR primitives into two properties, one - /// with just the value, and one with the id/extensions. - private void serializeInternal( - IReadOnlyDictionary members, - XmlWriter writer, - SerializationFilter? filter) - { - if (members is Resource r) - writer.WriteStartElement(r.TypeName, XmlNs.FHIR); + /// + /// Serializes the given dictionary with FHIR data into UTF8 encoded Json. + /// + public string SerializeToString( + Base element, + SerializationFilter? summary = default) => + SerializationUtil.WriteXmlToString(element, (o,w) => Serialize(o, w, summary)); - // Only throw if we don't have a mapping where we are expected to: when this is a subclass of Base. - if (!ClassMapping.TryGetMappingForType(members.GetType(), Release, out var mapping) && members is Base) - throw new InvalidOperationException($"Encountered type {members.GetType()}, which is a support POCO for FHIR, but does not " + - $"have sufficient metadata to be used by the serializer."); + /// + /// Serializes the given dictionary with FHIR data into Json, optionally skipping the "value" element. + /// + /// Not serializing the "value" element is useful when serializing FHIR primitives into two properties, one + /// with just the value, and one with the id/extensions. + private void serializeInternal( + Base element, + XmlWriter writer, + SerializationFilter? filter) + { + if (element is Resource r) + writer.WriteStartElement(r.TypeName, XmlNs.FHIR); - filter?.EnterObject(members, mapping); + // Only throw if we don't have a mapping where we are expected to: when this is a subclass of Base. + if (!ClassMapping.TryGetMappingForType(element.GetType(), Release, out var mapping)) + throw new InvalidOperationException($"Encountered type {element.GetType()}, which is a support POCO for FHIR, but does not " + + $"have sufficient metadata to be used by the serializer."); - serializeElement(members, writer, filter, mapping); + filter?.EnterObject(element, mapping); - filter?.LeaveObject(members, mapping); + serializeElement(element, writer, filter, mapping); - if (members is Resource) writer.WriteEndElement(); - } + filter?.LeaveObject(element, mapping); - private void serializeElement(IReadOnlyDictionary members, XmlWriter writer, SerializationFilter? filter, ClassMapping? mapping) - { - // Make sure that elements with attributes are serialized first. - var orderedMembers = members - .Select(m => (m, mapping: mapping?.FindMappedElementByName(m.Key))) - .OrderBy(p => p.mapping?.SerializationHint != XmlRepresentation.XmlAttr); + if (element is Resource) writer.WriteEndElement(); + } - foreach (var ((mKey, mValue), propertyMapping) in orderedMembers) - { - if (filter?.TryEnterMember(mKey, mValue, propertyMapping) == false) - continue; + private void serializeElement(Base element, XmlWriter writer, SerializationFilter? filter, ClassMapping? mapping) + { + // Make sure that elements with attributes are serialized first. + var orderedMembers = element + .Select(m => (m, mapping: mapping?.FindMappedElementByName(m.Key))) + .OrderBy(p => p.mapping?.SerializationHint != XmlRepresentation.XmlAttr); - var elementName = propertyMapping?.Choice == ChoiceType.DatatypeChoice ? - addSuffixToElementName(mKey, mValue) : mKey; + foreach (var ((mKey, mValue), propertyMapping) in orderedMembers) + { + if (filter?.TryEnterMember(mKey, mValue, propertyMapping) == false) + continue; - if (mValue is ICollection coll and not byte[]) - { - foreach (var value in coll) - serializeMemberValue(elementName, value, writer, filter); - } - else - serializeMemberValue(elementName, mValue, writer, filter); + var elementName = propertyMapping?.Choice == ChoiceType.DatatypeChoice ? + addSuffixToElementName(mKey, mValue) : mKey; - filter?.LeaveMember(mKey, mValue, propertyMapping); + if (mValue is ICollection coll and not byte[]) + { + foreach (var value in coll) + serializeMemberValue(elementName, value, writer, filter); } + else + serializeMemberValue(elementName, mValue, writer, filter); + + filter?.LeaveMember(mKey, mValue, propertyMapping); } + } - private static string addSuffixToElementName(string elementName, object elementValue) + private static string addSuffixToElementName(string elementName, object elementValue) + { + var typeName = elementValue switch { - var typeName = elementValue switch - { - IEnumerable ib => ib.FirstOrDefault()?.TypeName, - Base b => b.TypeName, - _ => null - }; + IEnumerable ib => ib.FirstOrDefault()?.TypeName, + Base b => b.TypeName, + _ => null + }; - return typeName is null ? elementName : elementName + char.ToUpperInvariant(typeName[0]) + typeName.Substring(1); - } + return typeName is null ? elementName : elementName + char.ToUpperInvariant(typeName[0]) + typeName[1..]; + } - private void serializeMemberValue(string elementName, object value, XmlWriter writer, SerializationFilter? filter) + private void serializeMemberValue(string elementName, object value, XmlWriter writer, SerializationFilter? filter) + { + switch (value) { - if (value is XHtml xhtml) - { + case XHtml xhtml: writer.WriteRaw(xhtml.Value); - } - else if (value is IReadOnlyDictionary complex) - { + break; + case Base complex: writer.WriteStartElement(elementName, XmlNs.FHIR); serializeInternal(complex, writer, filter); writer.WriteEndElement(); - } - else + break; + default: SerializePrimitiveValue(elementName, value, writer); - } - - /// - /// Serialize a primitive .NET value that may occur in the POCOs into XML. - /// - /// - /// To allow for future additions to the POCOs the list of primitives supported here - /// is larger than the set used by the current POCOs. Note that DateTimeOffsetc> and - /// byte[] are considered to be "primitive" values here (used as the value in - /// and ). - /// - protected virtual void SerializePrimitiveValue(string elementName, object value, XmlWriter writer) - { - if (value is null) return; // Don't write a null property - - var literal = value switch - { - int i32 => XmlConvert.ToString(i32), - uint ui32 => XmlConvert.ToString(ui32), - long i64 => XmlConvert.ToString(i64), - ulong ui64 => XmlConvert.ToString(ui64), - float si => XmlConvert.ToString(si), - double dbl => XmlConvert.ToString(dbl), - decimal dec => XmlConvert.ToString(dec), - // A little note about trimming and whitespaces. The spec says: - // "Implementers SHOULD trim leading and trailing whitespace before writing and SHOULD trim leading - // and trailing whitespace when reading attribute values (for XML schema conformance)" - string s => s.Trim(), - bool b => XmlConvert.ToString(b), - DateTimeOffset dto => ElementModel.Types.DateTime.FormatDateTimeOffset(dto), - byte[] bytes => Convert.ToBase64String(bytes), - _ => throw new FormatException($"There is no known serialization for type {value.GetType()} into an Xml primitive property value.") - }; - - writer.WriteAttributeString(elementName, ns: null, value: literal); + break; } } -} - -#if NETSTANDARD -file static class KvpExtensions -{ - public static void Deconstruct(this KeyValuePair kvp, out TKey key, out TValue value) + /// + /// Serialize a primitive .NET value that may occur in the POCOs into XML. + /// + /// + /// To allow for future additions to the POCOs the list of primitives supported here + /// is larger than the set used by the current POCOs. Note that DateTimeOffsetc> and + /// byte[] are considered to be "primitive" values here (used as the value in + /// and ). + /// + protected virtual void SerializePrimitiveValue(string elementName, object value, XmlWriter writer) { - key = kvp.Key; - value = kvp.Value; + var literal = value switch + { + int i32 => XmlConvert.ToString(i32), + uint ui32 => XmlConvert.ToString(ui32), + long i64 => XmlConvert.ToString(i64), + ulong ui64 => XmlConvert.ToString(ui64), + float si => XmlConvert.ToString(si), + double dbl => XmlConvert.ToString(dbl), + decimal dec => XmlConvert.ToString(dec), + // A little note about trimming and whitespaces. The spec says: + // "Implementers SHOULD trim leading and trailing whitespace before writing and SHOULD trim leading + // and trailing whitespace when reading attribute values (for XML schema conformance)" + string s => s.Trim(), + bool b => XmlConvert.ToString(b), + DateTimeOffset dto => ElementModel.Types.DateTime.FormatDateTimeOffset(dto), + byte[] bytes => Convert.ToBase64String(bytes), + _ => throw new FormatException($"There is no known serialization for type {value.GetType()} into an Xml primitive property value.") + }; + + writer.WriteAttributeString(elementName, ns: null, value: literal); } -} - -#endif - -#nullable restore \ No newline at end of file +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Serialization/FhirJsonConverter.cs b/src/Hl7.Fhir.Base/Serialization/FhirJsonConverter.cs index 39fc6a161d..c8b4f2541a 100644 --- a/src/Hl7.Fhir.Base/Serialization/FhirJsonConverter.cs +++ b/src/Hl7.Fhir.Base/Serialization/FhirJsonConverter.cs @@ -85,50 +85,6 @@ private FhirJsonConverter(IFhirSerializationEngine engine) this._engine = (PocoSerializationEngine)engine; } - /// - /// Constructs a that (de)serializes FHIR json for the - /// POCOs in a given assembly. - /// - /// The assembly containing classes to be used for deserialization. - [Obsolete("Using this directly is not recommended. Instead, try creating a converter using the .ForFhir static method of the JsonSerializerOptions class")] - public FhirJsonConverter( - Assembly assembly) : this(ModelInspector.ForAssembly(assembly)) - { - // nothing - } - - [Obsolete("Using this directly is not recommended. Instead, try creating a converter using the .ForFhir static method of the JsonSerializerOptions class")] - public FhirJsonConverter( - Assembly assembly, FhirJsonPocoSerializerSettings? serializerSettings = null, FhirJsonPocoDeserializerSettings? deserializerSettings = null, - Predicate? ignoreFilter = null) : - this(FhirSerializationEngineFactory.Custom(ModelInspector.ForAssembly(assembly), ignoreFilter ?? (_ => false), deserializerSettings, serializerSettings)) - { } - - /// - /// Constructs a that (de)serializes FHIR json for the - /// POCOs in a given assembly. - /// - /// The containing classes to be used for deserialization. - [Obsolete("Using this directly is not recommended. Instead, try creating a converter using the .ForFhir static method of the JsonSerializerOptions class")] - public FhirJsonConverter( - ModelInspector inspector) : this(FhirSerializationEngineFactory.Strict(inspector)) - { - } - - /// - /// Constructs a that (de)serializes FHIR json for the - /// POCOs in a given assembly. - /// - /// A custom deserializer to be used by the json converter. - /// A customer serializer to be used by the json converter. - /// Since the standard serializer/deserializer will allow you to override its behaviour to produce - /// custom behaviour, this constructor will allow the developer to use such custom serializers/deserializers instead - /// of the defaults. - [Obsolete("Using this directly is not recommended. Instead, try creating a converter using the .ForFhir static method of the JsonSerializerOptions class")] - public FhirJsonConverter(BaseFhirJsonPocoDeserializer deserializer, BaseFhirJsonPocoSerializer serializer) : this(FhirSerializationEngineFactory.WithCustomJsonSerializers(deserializer, serializer)) - { - } - /// /// Determines whether the specified type can be converted. /// diff --git a/src/Hl7.Fhir.Base/Serialization/FhirXmlBuilderExtensions.cs b/src/Hl7.Fhir.Base/Serialization/FhirXmlBuilderExtensions.cs index df7521861a..2329074192 100644 --- a/src/Hl7.Fhir.Base/Serialization/FhirXmlBuilderExtensions.cs +++ b/src/Hl7.Fhir.Base/Serialization/FhirXmlBuilderExtensions.cs @@ -71,10 +71,14 @@ public static async Task ToXmlAsync(this ISourceNode source, FhirXmlSeri public static string ToXml(this ITypedElement source, FhirXmlSerializationSettings settings = null) { if (source is not Base b) - return SerializationUtil.WriteXmlToString(writer => source.WriteTo(writer, settings), settings?.Pretty ?? false, settings?.AppendNewLine ?? false); - - var engine = FhirSerializationEngineFactory.Strict(ModelInspector.ForType(b.GetType())); - return ((PocoSerializationEngine)engine).SerializeToXml(b); + return SerializationUtil.WriteXmlToString(source, (s,w) => s.WriteTo(w, settings), + settings?.Pretty ?? false, settings?.AppendNewLine ?? false); + + // Note that this code is temporary, as we the above code will be re-instated here. It's therefore + // allowed to instantiate the serializer with a ModelInspector.ForType here, while we know this has + // problems (e.g. if the type is from Base or Conformance, it will not deduce the correct FHIR version). + var serializer = new BaseFhirXmlPocoSerializer(ModelInspector.ForType(b.GetType()).FhirRelease); + return serializer.SerializeToString(b); } [TemporarilyChanged] @@ -84,8 +88,11 @@ public static async Task ToXmlAsync(this ITypedElement source, FhirXmlSe return await SerializationUtil.WriteXmlToStringAsync(async writer => await source.WriteToAsync(writer, settings).ConfigureAwait(false), settings?.Pretty ?? false, settings?.AppendNewLine ?? false).ConfigureAwait(false); - var engine = FhirSerializationEngineFactory.Strict(ModelInspector.ForType(b.GetType())); - return ((PocoSerializationEngine)engine).SerializeToXml(b); + // Note that this code is temporary, as we the above code will be re-instated here. It's therefore + // allowed to instantiate the serializer with a ModelInspector.ForType here, while we know this has + // problems (e.g. if the type is from Base or Conformance, it will not deduce the correct FHIR version). + var serializer = new BaseFhirXmlPocoSerializer(ModelInspector.ForType(b.GetType()).FhirRelease); + return serializer.SerializeToString(b); } /// @@ -95,4 +102,4 @@ public static byte[] ToXmlBytes(this ITypedElement source, FhirXmlSerializationS public static async Task ToXmlBytesAsync(this ITypedElement source, FhirXmlSerializationSettings settings = null) => await SerializationUtil.WriteXmlToBytesAsync(async writer => await source.WriteToAsync(writer, settings).ConfigureAwait(false)).ConfigureAwait(false); } -} +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Serialization/engine/ElementModelSerializationEngine.cs b/src/Hl7.Fhir.Base/Serialization/engine/ElementModelSerializationEngine.cs index f9e14f092e..26bf0baa30 100644 --- a/src/Hl7.Fhir.Base/Serialization/engine/ElementModelSerializationEngine.cs +++ b/src/Hl7.Fhir.Base/Serialization/engine/ElementModelSerializationEngine.cs @@ -16,88 +16,70 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Net.Http.Headers; using System.Runtime.CompilerServices; -namespace Hl7.Fhir.Serialization +namespace Hl7.Fhir.Serialization; + +/// +/// This is an implementation of which uses the +/// "old" TypedElement-based parser and serializer. It is used as a switchable serialization +/// in the BaseFhirClient. +/// +internal class ElementModelSerializationEngine( + ModelInspector inspector, + FhirXmlParsingSettings xmlSettings, + FhirJsonParsingSettings jsonSettings, + PocoBuilderSettings pocoSettings) + : IFhirSerializationEngine { - /// - /// This is an implementation of which uses the - /// "old" TypedElement-based parser and serializer. It is used as a switchable serialization - /// in the BaseFhirClient. - /// - internal class ElementModelSerializationEngine : IFhirSerializationEngine + public static bool TryUnpackElementModelException(DeserializationFailedException dfe, [NotNullWhen(true)] out FormatException? fe) { - private readonly ModelInspector _inspector; - private readonly FhirXmlParsingSettings _xmlSettings; - private readonly FhirJsonParsingSettings _jsonSettings; - private readonly PocoBuilderSettings _pocoSettings; - - public ElementModelSerializationEngine( - ModelInspector inspector, - FhirXmlParsingSettings xmlSettings, - FhirJsonParsingSettings jsonSettings, - PocoBuilderSettings pocoSettings) + if (dfe.Exceptions.Count == 1 && dfe.Exceptions.Single() is ElementModelParserException empe) { - _inspector = inspector; - _xmlSettings = xmlSettings; - _jsonSettings = jsonSettings; - _pocoSettings = pocoSettings; + fe = (FormatException)empe.InnerException!; + return true; } - - public static bool TryUnpackElementModelException(DeserializationFailedException dfe, [NotNullWhen(true)] out FormatException? fe) + else { - if (dfe.Exceptions.Count == 1 && dfe.Exceptions.Single() is ElementModelParserException empe) - { - fe = (FormatException)empe.InnerException!; - return true; - } - else - { - fe = null; - return false; - } + fe = null; + return false; } + } - internal Base DeserializeBaseFromXml(string data) => deserialize(() => FhirXmlNode.Parse(data, settings: _xmlSettings)); - public Resource DeserializeFromXml(string data) => deserialize(() => FhirXmlNode.Parse(data, settings: _xmlSettings)); + public Resource DeserializeFromXml(string data) => deserialize(() => FhirXmlNode.Parse(data, settings: xmlSettings)); - public Resource DeserializeFromJson(string data) => deserialize(() => FhirJsonNode.Parse(data, settings: _jsonSettings)); + public Resource DeserializeFromJson(string data) => deserialize(() => FhirJsonNode.Parse(data, settings: jsonSettings)); - private Resource deserialize(Func deserializer) + private Resource deserialize(Func deserializer) + { + try + { + return (Resource)deserializer().ToPoco(inspector, null, pocoSettings); + } + catch (FormatException fe) { - try - { - return (Resource)deserializer().ToPoco(_inspector, null, _pocoSettings); - } - catch (FormatException fe) - { - // Note: this will catch StructuralTypeException as well, since that is a subclass of FormatException. - throw new DeserializationFailedException(null, new ElementModelParserException(fe)); - } - catch (InvalidOperationException ioe) - { - // Unfortunately, our ElementModel parsers treats resources without a type indicator with an InvalidOperationException. - // This should have been a parsing error, so let's turn it into one. - var fmt = new FormatException(ioe.Message); - throw new DeserializationFailedException(null, new ElementModelParserException(fmt)); - } + // Note: this will catch StructuralTypeException as well, since that is a subclass of FormatException. + throw new DeserializationFailedException(null, new ElementModelParserException(fe)); } + catch (InvalidOperationException ioe) + { + // Unfortunately, our ElementModel parsers treats resources without a type indicator with an InvalidOperationException. + // This should have been a parsing error, so let's turn it into one. + var fmt = new FormatException(ioe.Message); + throw new DeserializationFailedException(null, new ElementModelParserException(fmt)); + } + } - [TemporarilyChanged] - public string SerializeToXml(Base instance) => new CommonFhirXmlSerializer(_inspector).SerializeToString(instance); + public string SerializeToXml(Resource instance) => new CommonFhirXmlSerializer(inspector).SerializeToString(instance); - public string SerializeToJson(Resource instance) => new CommonFhirJsonSerializer(_inspector).SerializeToString(instance); + public string SerializeToJson(Resource instance) => new CommonFhirJsonSerializer(inspector).SerializeToString(instance); - internal class ElementModelParserException : CodedException - { - public const string ELEMENTMODEL_PARSER_EXCEPTION = "EMP001"; + internal class ElementModelParserException(FormatException fe) + : CodedException(ELEMENTMODEL_PARSER_EXCEPTION, fe.Message, fe) + { + public const string ELEMENTMODEL_PARSER_EXCEPTION = "EMP001"; - public ElementModelParserException(FormatException fe) : base(ELEMENTMODEL_PARSER_EXCEPTION, fe.Message, fe) - { - // Nothing - } - } + // Nothing } } diff --git a/src/Hl7.Fhir.Base/Serialization/engine/IFhirSerializationEngine.cs b/src/Hl7.Fhir.Base/Serialization/engine/IFhirSerializationEngine.cs index cc6b82f894..8a9a7eb458 100644 --- a/src/Hl7.Fhir.Base/Serialization/engine/IFhirSerializationEngine.cs +++ b/src/Hl7.Fhir.Base/Serialization/engine/IFhirSerializationEngine.cs @@ -10,111 +10,36 @@ #nullable enable using Hl7.Fhir.Model; -using System; using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; -using System.Text.Json; -using System.Xml; -namespace Hl7.Fhir.Serialization +namespace Hl7.Fhir.Serialization; + +/// +/// Represents an object that can serialize/deserialize FHIR data from the supported +/// serialization formats. +/// +public interface IFhirSerializationEngine { /// - /// Represents an object that can serialize/deserialize FHIR data from the supported - /// serialization formats. + /// Serialize a FHIR Resource POCO into a string of Json. /// - public interface IFhirSerializationEngine - { - /// - /// Serialize a FHIR Resource POCO into a string of Json. - /// - public string SerializeToJson(Resource instance); - - /// - /// Deserialize a Json string to a FHIR Resource POCO. - /// - /// Thrown when the deserializer encountered one or more errors in the FHIR Json format. - public Resource? DeserializeFromJson(string data); - - /// - /// Deserialize an XML string to a FHIR Resource POCO. - /// - /// Thrown when the deserializer encountered one or more errors in the FHIR Xml format. - /// Null if the data did not contain a resource, but another FHIR datatype. - public Resource? DeserializeFromXml(string data); - - /// - /// Serialize a FHIR Resource POCO into a string of Xml. - /// - [TemporarilyChanged] - public string SerializeToXml(Base instance); - } + public string SerializeToJson(Resource instance); /// - /// Extension methods for the interface when the underlying engine is NOT a legacy engine. + /// Deserialize a Json string to a FHIR Resource POCO. /// - public static class SerializationEngineExtensions - { - /// - /// Deserialize a FHIR Resource from a JSON reader. - /// - /// The resource, or null if the operation failed - /// Thrown if the underlying engine is a legacy engine - /// Thrown if a FHIR error was encountered in the data - public static Resource? SerializeReaderToJson(this IFhirSerializationEngine engine, ref Utf8JsonReader reader) - { - if (engine is not PocoSerializationEngine pse) - { - throw new InvalidOperationException("stream reading is not supported by legacy engines"); - } - - return pse.DeserializeFromJson(ref reader); - } - - /// - /// Deserialize a FHIR Resource from an XML reader. - /// - /// Thrown if the underlying engine is a legacy engine - /// Thrown if a FHIR error was encountered in the data - [TemporarilyChanged] - public static Base? SerializeReaderToXml(this IFhirSerializationEngine engine, XmlReader reader) - { - if (engine is not PocoSerializationEngine pse) - { - throw new InvalidOperationException("stream reading is not supported by legacy engines"); - } - - return pse.DeserializeFromXml(reader); - } + /// Thrown when the deserializer encountered one or more errors in the FHIR Json format. + public Resource? DeserializeFromJson(string data); - /// - /// Serialize a FHIR Resource to a JSON writer. - /// - /// - public static void SerializeToJsonWriter(this IFhirSerializationEngine engine, Resource instance, Utf8JsonWriter writer) - { - if (engine is not PocoSerializationEngine pse) - { - throw new InvalidOperationException("stream writing is not supported by legacy engines"); - } - - pse.SerializeToJsonWriter(instance, writer); - } - - /// - /// Serialize a FHIR Resource to an XML writer. - /// - /// Thrown if the underlying engine is a legacy engine - [TemporarilyChanged] - public static void SerializeToXmlWriter(this IFhirSerializationEngine engine, Base instance, XmlWriter writer) - { - if (engine is not PocoSerializationEngine pse) - { - throw new InvalidOperationException("stream writing is not supported by legacy engines"); - } - - pse.SerializeToXmlWriter(instance, writer); - } - } -} + /// + /// Deserialize an XML string to a FHIR Resource POCO. + /// + /// Thrown when the deserializer encountered one or more errors in the FHIR Xml format. + /// Null if the data did not contain a resource, but another FHIR datatype. + public Resource? DeserializeFromXml(string data); -#nullable restore + /// + /// Serialize a FHIR Resource POCO into a string of Xml. + /// + public string SerializeToXml(Resource instance); +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Serialization/engine/PocoSerializationEngine.cs b/src/Hl7.Fhir.Base/Serialization/engine/PocoSerializationEngine.cs index 5ae6925590..82c3fa789f 100644 --- a/src/Hl7.Fhir.Base/Serialization/engine/PocoSerializationEngine.cs +++ b/src/Hl7.Fhir.Base/Serialization/engine/PocoSerializationEngine.cs @@ -16,48 +16,47 @@ using System.Linq; using System.Reflection; -namespace Hl7.Fhir.Serialization +namespace Hl7.Fhir.Serialization; + +/// +/// This is an implementation of which uses the +/// new Poco-based parser and serializer, initialized with the default settings. +/// +internal partial class PocoSerializationEngine : IFhirSerializationEngine { - /// - /// This is an implementation of which uses the - /// new Poco-based parser and serializer, initialized with the default settings. - /// - internal partial class PocoSerializationEngine : IFhirSerializationEngine - { - private delegate (Base?, IEnumerable) TryDeserializer(); + private delegate (Base?, IEnumerable) TryDeserializer(); - private readonly ModelInspector _inspector; - internal Predicate IgnoreFilter { get; set; } + private readonly ModelInspector _inspector; + internal Predicate IgnoreFilter { get; set; } - internal PocoSerializationEngine(ModelInspector inspector, Predicate? ignoreFilter = null, FhirJsonPocoDeserializerSettings? jsonDeserializerSettings = null, FhirJsonPocoSerializerSettings? jsonSerializerSettings = null, FhirXmlPocoDeserializerSettings? xmlSettings = null) - { - _inspector = inspector; - IgnoreFilter = ignoreFilter ?? (_ => false); - _jsonDeserializerSettings = jsonDeserializerSettings ?? new FhirJsonPocoDeserializerSettings(); - _jsonSerializerSettings = jsonSerializerSettings ?? new FhirJsonPocoSerializerSettings(); - _xmlSettings = xmlSettings ?? new FhirXmlPocoDeserializerSettings(); - } + internal PocoSerializationEngine(ModelInspector inspector, Predicate? ignoreFilter = null, FhirJsonPocoDeserializerSettings? jsonDeserializerSettings = null, FhirJsonPocoSerializerSettings? jsonSerializerSettings = null, FhirXmlPocoDeserializerSettings? xmlSettings = null) + { + _inspector = inspector; + IgnoreFilter = ignoreFilter ?? (_ => false); + _jsonDeserializerSettings = jsonDeserializerSettings ?? new FhirJsonPocoDeserializerSettings(); + _jsonSerializerSettings = jsonSerializerSettings ?? new FhirJsonPocoSerializerSettings(); + _xmlSettings = xmlSettings ?? new FhirXmlPocoDeserializerSettings(); + } - internal PocoSerializationEngine(BaseFhirJsonPocoDeserializer deserializer, - BaseFhirJsonPocoSerializer serializer) - { - _jsonDeserializer = deserializer; - _jsonSerializer = serializer; - // dirty, but this constructor is really not supposed to be supported for much longer - var inspectorfield = - typeof(BaseFhirJsonPocoDeserializer).GetField("_inspector", BindingFlags.NonPublic | BindingFlags.Instance); - _inspector = (inspectorfield!.GetValue(_jsonDeserializer) as ModelInspector)!; - IgnoreFilter = _ => false; - _xmlSettings = new FhirXmlPocoDeserializerSettings(); - } + internal PocoSerializationEngine(BaseFhirJsonPocoDeserializer deserializer, + BaseFhirJsonPocoSerializer serializer) + { + _jsonDeserializer = deserializer; + _jsonSerializer = serializer; + // dirty, but this constructor is really not supposed to be supported for much longer + var inspectorfield = + typeof(BaseFhirJsonPocoDeserializer).GetField("_inspector", BindingFlags.NonPublic | BindingFlags.Instance); + _inspector = (inspectorfield!.GetValue(_jsonDeserializer) as ModelInspector)!; + IgnoreFilter = _ => false; + _xmlSettings = new FhirXmlPocoDeserializerSettings(); + } - private Base deserializeAndFilterErrors(TryDeserializer deserializer) - { - var (instance, issues) = deserializer(); - var relevantIssues = issues.Where(i => !IgnoreFilter(i)).ToList(); + private Base deserializeAndFilterErrors(TryDeserializer deserializer) + { + var (instance, issues) = deserializer(); + var relevantIssues = issues.Where(i => !IgnoreFilter(i)).ToList(); - return relevantIssues.Any() ? throw new DeserializationFailedException(instance, relevantIssues) : instance!; - } + return relevantIssues.Any() ? throw new DeserializationFailedException(instance, relevantIssues) : instance!; } } diff --git a/src/Hl7.Fhir.Base/Serialization/engine/PocoSerializationEngine_Xml.cs b/src/Hl7.Fhir.Base/Serialization/engine/PocoSerializationEngine_Xml.cs index d6fe543858..385ff53e97 100644 --- a/src/Hl7.Fhir.Base/Serialization/engine/PocoSerializationEngine_Xml.cs +++ b/src/Hl7.Fhir.Base/Serialization/engine/PocoSerializationEngine_Xml.cs @@ -34,18 +34,11 @@ public Resource DeserializeFromXml(string data) }); } - internal Base DeserializeBaseFromXml(string data) - { - return deserializeAndFilterErrors(() => - { - _ = getXmlDeserializer().TryDeserializeResource(data, out var instance, out var issues); - return (instance, issues); - }); - } - /// - [TemporarilyChanged] - public string SerializeToXml(Base instance) => getXmlSerializer().SerializeToString(instance); + public string SerializeToXml(Resource instance) => getXmlSerializer().SerializeToString(instance); + + + internal string SerializeElementToXml(Base instance) => getXmlSerializer().SerializeToString(instance); /// /// Deserializes a resource from an XML reader @@ -61,26 +54,11 @@ public Resource DeserializeFromXml(XmlReader reader) }); } - /// - /// Deserializes an element from an XML reader - /// - /// The target type of the object - /// The XML reader - /// A POCO representation of the input read by the reader - public Base DeserializeElementFromXml(Type targetType, XmlReader reader) - { - return deserializeAndFilterErrors(() => - { - _ = getXmlDeserializer().TryDeserializeElement(targetType, reader, out var instance, out var issues); - return (instance, issues); - }); - } - + /// /// Serializes an element to the supplied writer /// /// An instance of Base or any of its children /// The XML writer - [TemporarilyChanged] - public void SerializeToXmlWriter(Base instance, XmlWriter writer) => getXmlSerializer().Serialize(instance, writer); + internal void SerializeToXmlWriter(Base instance, XmlWriter writer) => getXmlSerializer().Serialize(instance, writer); } \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Serialization/engine/SerializationEngineExtensions.cs b/src/Hl7.Fhir.Base/Serialization/engine/SerializationEngineExtensions.cs new file mode 100644 index 0000000000..6e85fca356 --- /dev/null +++ b/src/Hl7.Fhir.Base/Serialization/engine/SerializationEngineExtensions.cs @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2024, Firely (info@fire.ly) and contributors + * See the file CONTRIBUTORS for details. + * + * This file is licensed under the BSD 3-Clause license + * available at https://raw.githubusercontent.com/FirelyTeam/firely-net-sdk/master/LICENSE + */ + +#nullable enable +using Hl7.Fhir.Model; +using System; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Xml; + +namespace Hl7.Fhir.Serialization; + +/// +/// Extension methods for the interface when the underlying engine is NOT a legacy engine. +/// +public static class SerializationEngineExtensions +{ + /// + /// Deserialize a FHIR Resource from a . + /// + /// The resource, or null if the operation failed + /// Thrown if the underlying engine is a legacy engine + /// Thrown if a FHIR error was encountered in the data + public static Resource DeserializeFromJsonReader(this IFhirSerializationEngine engine, ref Utf8JsonReader reader) + { + if (engine is not PocoSerializationEngine pse) + { + throw new InvalidOperationException("stream reading is not supported by legacy engines"); + } + + return pse.DeserializeFromJson(ref reader); + } + + /// + /// Deserialize a FHIR Resource from an . + /// + /// Thrown if the underlying engine is a legacy engine + /// Thrown if a FHIR error was encountered in the data + public static Resource DeserializeFromXmlReader(this IFhirSerializationEngine engine, XmlReader reader) + { + if (engine is not PocoSerializationEngine pse) + { + throw new InvalidOperationException("stream reading is not supported by legacy engines"); + } + + return pse.DeserializeFromXml(reader); + } + + /// + /// Serialize a FHIR Resource to a JSON writer. + /// + /// + public static void SerializeToJsonWriter(this IFhirSerializationEngine engine, Resource instance, Utf8JsonWriter writer) + { + if (engine is not PocoSerializationEngine pse) + { + throw new InvalidOperationException("stream writing is not supported by legacy engines"); + } + + pse.SerializeToJsonWriter(instance, writer); + } + + /// + /// Serialize a FHIR Resource to an . + /// + /// Thrown if the underlying engine is a legacy engine + public static void SerializeToXmlWriter(this IFhirSerializationEngine engine, Resource instance, XmlWriter writer) + { + if (engine is not PocoSerializationEngine pse) + { + throw new InvalidOperationException("stream writing is not supported by legacy engines"); + } + + pse.SerializeToXmlWriter(instance, writer); + } + + /// + /// Serialize an element (a child of a resource) to a . + /// + /// This method allows you to serialize + /// a subtree of data to XML, which can be useful for debug and display purposes. Note that the + /// FHIR standard does not prescribe how this is done, so the output of this function is by definition + /// non standard and should not be used for production purposes. + public static void SerializeElementToXmlWriter(this IFhirSerializationEngine engine, Base instance, XmlWriter writer) + { + if (engine is not PocoSerializationEngine pse) + { + throw new InvalidOperationException("stream writing is not supported by legacy engines"); + } + + pse.SerializeToXmlWriter(instance, writer); + } + + /// + /// Serialize an element (a child of a resource) to a string of Xml. + /// + /// This method allows you to serialize + /// a subtree of data to XML, which can be useful for debug and display purposes. Note that the + /// FHIR standard does not prescribe how this is done, so the output of this function is by definition + /// non standard and should not be used for production purposes. + public static void SerializeElementToXml(this IFhirSerializationEngine engine, Base instance) + { + if (engine is not PocoSerializationEngine pse) + { + throw new InvalidOperationException("stream writing is not supported by legacy engines"); + } + + pse.SerializeElementToXml(instance); + } +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Serialization.R4.Tests/RoundtripSignature.cs b/src/Hl7.Fhir.Serialization.R4.Tests/RoundtripSignature.cs index d023af4d2b..002abae8f5 100644 --- a/src/Hl7.Fhir.Serialization.R4.Tests/RoundtripSignature.cs +++ b/src/Hl7.Fhir.Serialization.R4.Tests/RoundtripSignature.cs @@ -58,11 +58,11 @@ public void TypedElementHasCorrectInfo() } [TestMethod] - [Ignore("These should not be exactly equal!")] - [TemporarilyChanged] + [Ignore("Doesn't work for our ModelInspector.ForType() hack")] + [TemporarilyChanged] public void WorksWithTypedElementSerializers() { - var sig = new Bundle() { Signature = new Signature() { Who = new ResourceReference("http://nu.nl") } }; + var sig = new Bundle { Signature = new Signature() { Who = new ResourceReference("http://nu.nl") } }; var json = sig.ToTypedElement().ToJson(); //json.Should().Contain("\"who\""); var sig2 = FhirJsonNode.Parse(json).ToPoco(); @@ -85,4 +85,4 @@ public void WorksWithPocoSerializers() } } -} +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Serialization.Shared.Tests/SerializePartialTree.cs b/src/Hl7.Fhir.Serialization.Shared.Tests/SerializePartialTree.cs index a56322f628..a2a2bc58f9 100644 --- a/src/Hl7.Fhir.Serialization.Shared.Tests/SerializePartialTree.cs +++ b/src/Hl7.Fhir.Serialization.Shared.Tests/SerializePartialTree.cs @@ -52,12 +52,6 @@ public async Tasks.Task CanSerializeSubtree() var subnavPoco = navPoco.Children("photo").First(); await testSubtree(subnavXml, subnavJson, subnavPoco); - // And on a child that's a primitive datatype - //subnavXml = navXml.Children("id").First(); - //subnavJson = navJson.Children("id").First(); - //subnavPoco = navPoco.Children("id").First(); - //testSubtree(subnavXml, subnavJson, subnavPoco); - // And on a contained resource subnavXml = navXml.Children("contained").First(); subnavJson = navJson.Children("contained").First(); @@ -147,4 +141,4 @@ public static ITypedElement ParseToTypedElement(string xml, string type, IStruct } -} +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Shared.Tests/ElementModel/PocoTypedElementTests.cs b/src/Hl7.Fhir.Shared.Tests/ElementModel/PocoTypedElementTests.cs index 316a89f2ad..08ba78d656 100644 --- a/src/Hl7.Fhir.Shared.Tests/ElementModel/PocoTypedElementTests.cs +++ b/src/Hl7.Fhir.Shared.Tests/ElementModel/PocoTypedElementTests.cs @@ -197,7 +197,8 @@ public async Tasks.Task ValidateFiveWs() [TestMethod] - [Ignore("This design is still under discussion")] + [Ignore("We implemented ITypedElement on top of Base, but that does not track which version of FHIR the element is from," + + "so we cannot select the right type of the elements here since they diverge between versions.")] [TemporarilyChanged] public void CheckTypeOfElementDefinitionMembers() { @@ -233,4 +234,4 @@ public void CheckTypeOfElementDefinitionMembers() } } -} +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Shims.STU3AndUp/ElementModel/TypedElementExtensions.cs b/src/Hl7.Fhir.Shims.STU3AndUp/ElementModel/TypedElementExtensions.cs index 002593d951..dce33d8288 100644 --- a/src/Hl7.Fhir.Shims.STU3AndUp/ElementModel/TypedElementExtensions.cs +++ b/src/Hl7.Fhir.Shims.STU3AndUp/ElementModel/TypedElementExtensions.cs @@ -9,11 +9,13 @@ #nullable enable using Hl7.Fhir.Model; +using System.Runtime.CompilerServices; namespace Hl7.Fhir.ElementModel { public static class TypedElementExtensions { + [TemporarilyChanged] // This should be restored to use the original ITE stack. public static ITypedElement ToTypedElement(this Base @base, string? rootName = null) => @base.WithScopeInfo(@base.BuildRoot(rootName)); } diff --git a/src/Hl7.Fhir.Support.Poco.Tests/NewPocoSerializers/FhirXmlDeserializationTests.cs b/src/Hl7.Fhir.Support.Poco.Tests/NewPocoSerializers/FhirXmlDeserializationTests.cs index c01b1a23e0..5a39ae7433 100644 --- a/src/Hl7.Fhir.Support.Poco.Tests/NewPocoSerializers/FhirXmlDeserializationTests.cs +++ b/src/Hl7.Fhir.Support.Poco.Tests/NewPocoSerializers/FhirXmlDeserializationTests.cs @@ -506,22 +506,24 @@ static void test(IDeserializationValidator validator) [TestMethod] public void TestNewXmlParserNarrativeParsing() { - var patient = new Patient { Id = "example" }; - patient.Text = new Narrative() { Status = Narrative.NarrativeStatus.Generated, Div = "
some test data
" }; - var serializer = new BaseFhirXmlPocoSerializer(Specification.FhirRelease.STU3); - var actual = SerializationUtil.WriteXmlToString(patient, (o, w) => serializer.Serialize(o, w)); + var patient = new Patient + { + Id = "example", + Text = new Narrative() + { + Status = Narrative.NarrativeStatus.Generated, + Div = "
some test data
" + } + }; - //// now parse this back out with the old parser - //var op = new Hl7.Fhir.Serialization.FhirXmlParser().Parse(patientXML); - //Assert.AreEqual(patient.Text.Div, op.Text.Div, "Old narrative should be the same"); + var serializer = new BaseFhirXmlPocoSerializer(Specification.FhirRelease.STU3); + var actual = serializer.SerializeToString(patient); // now parse this back out with the new parser - BaseFhirXmlPocoDeserializer ds = getTestDeserializer(new()); - using (var reader = SerializationUtil.XmlReaderFromXmlText(actual)) - { - var np = ds.DeserializeResource(reader) as Patient; - Assert.AreEqual(patient.Text.Div, np.Text.Div, "New narrative should be the same"); - } + BaseFhirXmlPocoDeserializer ds = getTestDeserializer(new FhirXmlPocoDeserializerSettings()); + + var np = ds.DeserializeResource(actual).Should().BeOfType().Subject; + Assert.AreEqual(patient.Text.Div, np.Text.Div, "New narrative should be the same"); } [TestMethod] diff --git a/src/firely-net-sdk.props b/src/firely-net-sdk.props index 6088dd6c00..e120a9edae 100644 --- a/src/firely-net-sdk.props +++ b/src/firely-net-sdk.props @@ -66,4 +66,3 @@ -