davebronson/clear-hl7-net

Add methods for Segment and Type classes to deserialize from a delimited string to HL7 objects

davebronson opened this issue · 8 comments

Enhance library to perform deserialization from a delimited string to HL7 objects.

string input = ReceiveHl7Message();
Message message = messageRepo.Deserialize(input);
  • Message
  • Segment
  • Type
  • Include Unescape() of strings

Hi,

Is this being looked at?

Regards,

Hi lawania. I haven't begun to work on this enhancement yet, but knowing that someone could make use of it will help me prioritize. I'll create a new branch for the effort. Thanks!

Branch created: add-deserialization-methods

Hello Dave,

Do you need any help or brainstorming, in terms of how to achieve this?

Certainly, I would welcome any suggestions you might have. I've started testing with the sample method below, which is intended to live in the ClearHl7.V290.Types.Address class. I'll commit what I have to the new branch so you can see this, and the related unit test, in action.

The general idea here is that each class would be responsible for deserializing its own "section" of the HL7 message structure, in the same way that it's currently responsible for serializing. The deserialization would be kicked off at the Message level, and then automatically inflate the class hierarchy on its way down through the structure of Segments, Types, and Sub-Types. I like the separation of logic with this design.

That being said, using the factory pattern would be a better fit for this sort of thing (object initialization). Though I'm not crazy about adding another ~2500 (or 5000!) classes to this library to make that happen. I'd welcome any suggestions people may have that would help sway me in one direction.

It's a work in progress. One concern I do have is the potential for a lot of string allocation when using String.Split(). There are faster ways to parse these strings, and with less memory usage, but maybe at the expense of code complexity.

/// <summary>
/// Initializes properties of this instance with values parsed from the given delimited string.
/// </summary>
/// <param name="delimitedString">A string representation that will be deserialized into the object instance.</param>
/// <returns>A reference to this instance after the operation has completed.</returns>
public Address FromDelimitedString(string delimitedString)
{
    string separator = IsSubcomponent ? Configuration.SubcomponentSeparator : Configuration.ComponentSeparator;
    string[] segments = delimitedString == null ? new string[] { } : delimitedString.Split(separator.ToCharArray());
    
    StreetAddress = segments.ElementAtOrDefault(0);
    OtherDesignation = segments.ElementAtOrDefault(1);
    City = segments.ElementAtOrDefault(2);
    StateOrProvince = segments.ElementAtOrDefault(3);
    ZipOrPostalCode = segments.ElementAtOrDefault(4);
    Country = segments.ElementAtOrDefault(5);
    AddressType = segments.ElementAtOrDefault(6);
    OtherGeographicDesignation = segments.ElementAtOrDefault(7);

    return this;
}
// A call to this method might look like:
Address someAddress = new Address().FromDelimitedString("123 Main Street^Suite 3^Anytown^NY");

Hello David,

Apologies for the delayed response completely forgot due to other priority work.

In my opinion, types could potentially use the method you mentioned because the type might have another type in one of its property. It becomes hard to track whether it's for the main component or sub-component.
Segment, I like your idea of having ISegment, but it would be good to have a base class that all segment inherits. Within the segment itself so we can place some common methods there. We can also add a new custom attribute, something like FieldAttribute, which can have the property representing the actual segment value. for example, in PID segment 1st two properties will look like this

` ///


/// Gets or sets the rank, or ordinal, which describes the place that this Segment resides in an ordered list of Segments.
///

public int Ordinal { get; set; }

    /// <summary>
    /// PID.1 - Set ID - PID.
    /// </summary>
    [FieldAttribute(1)]
    public uint? SetIdPid { get; set; }

    /// <summary>
    /// PID.2 - Patient ID.
    /// </summary>
    [FieldAttribute(2)]
    public string PatientId { get; set; }`

Now, we can use reflection in the base class constructor where the segment string could be passed and based on the split position and the field attribute, it will auto-assign the value—something like this.

`namespace ClearHl7.V290
{
public class SegmentBase : ISegment
{

    public string Id { get; } = "PID";

    /// <summary>
    /// Gets or sets the rank, or ordinal, which describes the place that this Segment resides in an ordered list of Segments.
    /// </summary>
    /// 
    [FieldAttribute(0)]
    public int Ordinal { get; set; }

    /// <summary>
    /// Initializes a new instance of the <see cref="SegmentBase"/> class.
    /// </summary>
    /// <param name="Segment">The segment.</param>
    /// <param name="Separator">The separator.</param>
    public SegmentBase(String Segment)
    {
        if (Segment == null) return;
        // We have the information need to break the message so now we can start populating the object


        var SegmentValues = Segment.Split(Convert.ToChar(Configuration.FieldSeparator));

        var PropList = SortedFieldAndComponentMembers(this);

        foreach (var item in PropList)
        {
            item.Key.SetValue(this, SegmentValues.ValueExtract(item), null);
        }
    }

    /// <summary>
    /// Returns a delimited string representation of this instance.
    /// </summary>
    /// <returns>A string.</returns>
    public string ToDelimitedString()
    {
        String Result = "";

        var PropList = SortedFieldAndComponentMembers(this);

        System.Globalization.CultureInfo culture = System.Globalization.CultureInfo.CurrentCulture;

        return string.Format(
                            culture,
                            StringHelper.StringFormatSequence(0, 14, Configuration.FieldSeparator),
                            Id,
                            PropList.Select(x=>x.Key.GetValue(x,null))
                            ).TrimEnd(Configuration.FieldSeparator.ToCharArray());

    }
    public static Object ValueExtract(this IEnumerable<String> value, KeyValuePair<System.Reflection.PropertyInfo, FieldAttribute> Prop)
    {
        try
        {
            var SubValue = value.ElementAtOrDefault(Prop.Value.Position.GetValueOrDefault());

            //For date time 
            if (Prop.Key.PropertyType == typeof(DateTime) || Prop.Key.PropertyType == typeof(DateTime?))
            {
                DateTime DateValue;
                if (DateTime.TryParseExact(SubValue, Consts.DateTimeFormatPrecisionSecond, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateValue)) return DateValue;
                else return null;
            }

            if (Prop.Key.PropertyType == typeof(Double) || Prop.Key.PropertyType == typeof(Double?))
            {
                Double DoubleValue;
                if (Double.TryParse(SubValue, out DoubleValue)) return DoubleValue;
                else return null;
            }

            //Now lets do with all the type in the same manner 

            if (Prop.Key.PropertyType == typeof(CodedWithExceptions)) return new CodedWithExceptions(SubValue);
            //........ continue for the rest


            if (Prop.Key.PropertyType.IsEnum)
            {
                return Enum.Parse(Prop.Key.PropertyType, SubValue);
            }

            if (SubValue != null) { return (object)Convert.ChangeType(SubValue, Prop.Key.PropertyType); }
        }
        catch (Exception )
        {

        }
        return null;
    }

    /// <summary>
    /// Returns the list of properties based on the new field attributed we have added
    /// </summary>
    /// <param name="Value"></param>
    /// <returns></returns>
    public static Dictionary<PropertyInfo, FieldAttribute> SortedFieldAndComponentMembers(this Object Value)
    {

        Dictionary<PropertyInfo, FieldAttribute> info = new Dictionary<PropertyInfo, FieldAttribute>();

        try
        {


            info = Value.GetType()
                .GetProperties(BindingFlags.Instance | BindingFlags.Public)
                                    .Select(x => new
                                    {
                                        Property = x,
                                        Attribute = (FieldAttribute)Attribute.GetCustomAttribute(x, typeof(FieldAttribute), false)
                                    })
                .Where(a => a.Attribute != null)
                .OrderBy(f => f.Attribute.Position)
                .ToDictionary(p => p.Property, a => a.Attribute);
        }
        catch (Exception)
        {
        }
        return info;
    }

}

}`

Now please its not finished or tested code just an idea. What is allows is we have to only maintain the properties and their attribute with position and improves support for future. Although, adding the attribute on each property isn't small task either.

Just a thought !

Thank you for putting time into this, @lawania. I see where you're going with your sample code. A move to using an abtract base class with generic methods for serialization and deserialization would result in a significant reduction in duplicated code. And it would also move logic out of the Type and Segment classes, which I would support. However I do have some concerns with this approach:

  • Issues with performance when using Reflection to traverse the object tree, as opposed to the current methods that directly interact with types/properties. This may or may not be of any major significance, so it would have to be tested to determine if/how much reflection actually hurts performance.
  • Issues with performance when repeatedly traversing the if/else logic to determine the correct type for each property.
  • Dealing with potential edge cases that may require special processing that would fail under the default logic.
  • This would be a breaking change for serialization (moving ToDelimitedString() logic out of the Types and Segments).
  • It could get messy performing serialization/deserialization when the property requires a format that's not consistent throughout the library, like DateTime and decimal. You could, of course, solve that by adding additional decorations to those properties.

I'm very hesitant to reverse course and refactor at this point, especially when I believe the proposed design has a decent chance of degrading performance (i.e. when using Reflection).

Serialization method samples (subject to change)

using ClearHl7.Serialization;
using ClearHl7.V290;
using ClearHl7.V290.Segments;
using ClearHl7.V290.Types;

String --> ClearHl7 object

// Use Deserialize() to receive a new, populated instance
IMessage message = MessageSerializer.Deserialize<Message>(messageString);
ISegment absSegment = SegmentSerializer.Deserialize<AbsSegment>(absString);
IType addressType = TypeSerializer.Deserialize<Address>(addressString, false);

// Use FromDelimitedString() to populate an existing instance
message.FromDelimitedString(messageString);
absSegment.FromDelimitedString(absString);
addressType.FromDelimitedString(addressString);

ClearHl7 object --> String

// Use Serialize()
string messageString = MessageSerializer.Serialize(message);
string absString = SegmentSerializer.Serialize(absSegment);
string addressString = TypeSerializer.Serialize(addressType, false);

// Use ToDelimitedString()
string messageString = message.ToDelimitedString();
string absString = absSegment.ToDelimitedString();
string addressString = addressType.ToDelimitedString();