Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
2 changes: 2 additions & 0 deletions src/Hl7.Fhir.Base/Model/Base.Dictionary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
using System.Collections.Generic;
using System.Linq;

#nullable enable

namespace Hl7.Fhir.Model;

public abstract partial class Base: IReadOnlyDictionary<string,object>, IDictionary<string, object>
Expand Down
504 changes: 250 additions & 254 deletions src/Hl7.Fhir.Base/Serialization/BaseFhirJsonPocoSerializer.cs

Large diffs are not rendered by default.

284 changes: 137 additions & 147 deletions src/Hl7.Fhir.Base/Serialization/BaseFhirXmlPocoSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,188 +19,178 @@
using System.Linq;
using System.Xml;

namespace Hl7.Fhir.Serialization
namespace Hl7.Fhir.Serialization;

/// <summary>
/// Serializes the contents of a POCO according to the rules of FHIR Xml serialization.
/// </summary>
/// <remarks>The serializer uses the format documented in https://www.hl7.org/fhir/xml.html.
/// </remarks>
public class BaseFhirXmlPocoSerializer
{
/// <summary>
/// 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.
/// </summary>
/// <remarks>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.
/// </remarks>
public class BaseFhirXmlPocoSerializer
public FhirRelease Release { get; }

/// <summary>
/// Construct a new serializer for a specific release of FHIR.
/// </summary>
public BaseFhirXmlPocoSerializer(FhirRelease release)
{
/// <summary>
/// The release of FHIR for which this serializer is configured.
/// </summary>
public FhirRelease Release { get; }

/// <summary>
/// Construct a new serializer for a specific release of FHIR.
/// </summary>
public BaseFhirXmlPocoSerializer(FhirRelease release)
{
Release = release;
}
Release = release;
}

/// <summary>
/// Serializes the given dictionary with FHIR data into Json.
/// </summary>
public void Serialize(IReadOnlyDictionary<string, object> members, XmlWriter writer, SerializationFilter? summary = default)
/// <summary>
/// Serializes the given dictionary with FHIR data into Json.
/// </summary>
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();
}

/// <summary>
/// Serializes the given dictionary with FHIR data into UTF8 encoded Json.
/// </summary>
public string SerializeToString(IReadOnlyDictionary<string, object> members, SerializationFilter? summary = default) =>
SerializationUtil.WriteXmlToString(w => Serialize(members, w, summary));

/// <summary>
/// Serializes the given dictionary with FHIR data into Json, optionally skipping the "value" element.
/// </summary>
/// <remarks>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.</remarks>
private void serializeInternal(
IReadOnlyDictionary<string, object> members,
XmlWriter writer,
SerializationFilter? filter)
{
if (members is Resource r)
writer.WriteStartElement(r.TypeName, XmlNs.FHIR);
/// <summary>
/// Serializes the given dictionary with FHIR data into UTF8 encoded Json.
/// </summary>
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.");
/// <summary>
/// Serializes the given dictionary with FHIR data into Json, optionally skipping the "value" element.
/// </summary>
/// <remarks>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.</remarks>
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<string, object> 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<Base> ib => ib.FirstOrDefault()?.TypeName,
Base b => b.TypeName,
_ => null
};
IEnumerable<Base> 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<string, object> complex)
{
break;
case Base complex:
writer.WriteStartElement(elementName, XmlNs.FHIR);
serializeInternal(complex, writer, filter);
writer.WriteEndElement();
}
else
break;
default:
SerializePrimitiveValue(elementName, value, writer);
}

/// <summary>
/// Serialize a primitive .NET value that may occur in the POCOs into XML.
/// </summary>
/// <remarks>
/// 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 <c>DateTimeOffset</c>c> and
/// <c>byte[]</c> are considered to be "primitive" values here (used as the value in
/// <see cref="Instant"/> and <see cref="Base64Binary"/>).
/// </remarks>
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<TKey, TValue>(this KeyValuePair<TKey, TValue> kvp, out TKey key, out TValue value)
/// <summary>
/// Serialize a primitive .NET value that may occur in the POCOs into XML.
/// </summary>
/// <remarks>
/// 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 <c>DateTimeOffset</c>c> and
/// <c>byte[]</c> are considered to be "primitive" values here (used as the value in
/// <see cref="Instant"/> and <see cref="Base64Binary"/>).
/// </remarks>
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
}
44 changes: 0 additions & 44 deletions src/Hl7.Fhir.Base/Serialization/FhirJsonConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,50 +85,6 @@ private FhirJsonConverter(IFhirSerializationEngine engine)
this._engine = (PocoSerializationEngine)engine;
}

/// <summary>
/// Constructs a <see cref="JsonConverter{T}"/> that (de)serializes FHIR json for the
/// POCOs in a given assembly.
/// </summary>
/// <param name="assembly">The assembly containing classes to be used for deserialization.</param>
[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<CodedException>? ignoreFilter = null) :
this(FhirSerializationEngineFactory.Custom(ModelInspector.ForAssembly(assembly), ignoreFilter ?? (_ => false), deserializerSettings, serializerSettings))
{ }

/// <summary>
/// Constructs a <see cref="JsonConverter{T}"/> that (de)serializes FHIR json for the
/// POCOs in a given assembly.
/// </summary>
/// <param name="inspector">The <see cref="ModelInspector" /> containing classes to be used for deserialization.</param>
[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))
{
}

/// <summary>
/// Constructs a <see cref="JsonConverter{T}"/> that (de)serializes FHIR json for the
/// POCOs in a given assembly.
/// </summary>
/// <param name="deserializer">A custom deserializer to be used by the json converter.</param>
/// <param name="serializer">A customer serializer to be used by the json converter.</param>
/// <remarks>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.</remarks>
[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))
{
}

/// <summary>
/// Determines whether the specified type can be converted.
/// </summary>
Expand Down
Loading