Documentation

Validate EDI with templates

Article author
Admin
  • Updated

How to validate EDI transactions with EDI templates?

It is essential that valid documents are exchanged between trading partners. Accuracy is one of the benefits that EDI provides over paper document processing, and estimates suggest that providing accurate data results in 30% faster delivery time.

EDI tools for .NET allows you to quickly establish if a message is valid or not, and provides you with the exact position and reason for any inaccuracies. To determine this, it uses our custom validation attributes to check for mandatory items, too many or too few repetitions, the correct length, data type or EDI code of a data element, etc.

Internally, validating an EDI transaction with EDI Tools for .NET is a three-step process:

  1. Apply validation attributes in the EDI template.
  2. Validate a .NET object that represents an EDI transaction by calling the IsValid() method
  3. Report all issues in a collection with details for the position and the reason for the error. 

The green path below depicts the validation of an EDI transaction:

  • An EDI POCO (.NET object that represents an EDI transaction)
  • Is processed through an EDI validator (EdiFabric)
  • To produce a Validation Context, e.g. a collection of errors, their positions, and reasons for failure

edi template standard

 

EDI Template Validation Attributes

EDI templates are essentially .NET models, as in MVC (model-view-controller). In the same way, as .NET models are being validated using System.ComponentModel.DataAnnotations namespace, EDI templates offer a set of validation attributes and an IsValid() method that validates the model (or the .NET object that represents the EDI transaction).

New attributes can be created by inheriting from ValidationAttribute base class.

When multiple attributes are combined on the same class property, validation is executed according to the attribute's validation order.

The following attributes are used to enable validation.

 

Usage

[Required]
 [Pos(2)]
 public BIG BIG { get; set; }

Validation Order: 1

All Mandatory items are annotated with RequiredAttribute. This attribute can be applied to any property. Items that are not annotated are considered to be Optional.

 

Repetitions

 [ListCount(100)]
 [Pos(3)]
 public List<NTE> NTE { get; set; }

Validation Order: 2

To control the number of repetitions annotate repeated items with ListCountAttribute. The first parameter is the upper limit of how many items are allowed in the list. You can also set the minimum limit if needed by using the constructor with two parameters. This attribute should only be applied to properties of generic type List<> otherwise it will be discarded.

 

Length of Data Element

 [StringLength(1, 10)]
 [Pos(1)]
 public string NumberofIncludedSegments_01 { get; set; }

Validation Order: 3

To control the length of data elements annotate them with StringLengthAttribute. The first parameter is the lower limit of the string length. The second parameter is the upper limit of the string length. This attribute should only be applied to properties of type string; otherwise, it will be discarded.

 

Type of Data Element

 [DataElement("96", typeof(X12_AN))]
 [Pos(1)]
 public string NumberofIncludedSegments_01 { get; set; }

Validation Order: 4

To set the data type of data elements annotate them with DataElementAttribute. The first parameter is the EDI identifier of the data element. The second parameter is the type of the data element. This attribute should only be applied to properties of type string; otherwise, it will be discarded. If the referred type is annotated with EdiCodesAttribute, the data element value will be validated against the list of allowed EDI codes.

 

Sequence Counter

[SeqCount]
 [Pos(1)]
 public LX_HeaderNumber LX_ServiceLineNumber { get; set; }

Validation Order: 10

Check that the value at the specified position in each item in the containing list is correct. For example, in LX loops, checks that the values in the first data element, are 1, 2, 3, etc., matching them to the index of the repeating LX. 

 

EDI Template Conditional Attributes

Certain scenarios require cross-segment or cross-field validation. For example in an address, the postcode is considered valid only when the city is also supplied. A data element's usage can depend on the occurrence of other data elements.

The conditional attributes are used in conjunction with all other validation attributes and are executed in accordance with the general validation order.

Conditional attributes can be used for HIPAA SNIP Type 4 validation and cover the Syntax Notes rules for HIPAA 5010.

The following attributes are used to enable conditional validation and can be applied to any data element property.

 

Conditional

 [Conditional(5)]
 [StringLength(1, 18)]
 [DataElement("782", typeof(X12_R))]
 [Pos(6)]
 public string AdjustmentAmount_06 { get; set; }

Validation Order: 8

If the annotated EDI data element is not null then all elements at the specified positions must also be not null.

Example: In N4 segment CountrySubdivisionCode_07 can only exist if CountryCode_04 exists.

 

ConditionalAny

 [ConditionalAny(6, 7)]
 [StringLength(1, 5)]
 [DataElement("1034", typeof(X12_ID))]
 [Pos(5)]
 public string AdjustmentReasonCode_05 { get; set; }

Validation Order: 9

If the annotated EDI data element is not null then at least one of the EDI data elements at the specified positions must also be not null. The attribute is applied to only one of the EDI data elements included in the condition.

Example: In MEA segment if RangeMinimum_05 exist then either CompositeUnitofMeasure_04 or IndustryCode_12 must exist.

 

Exclusion

 [Exclusion(7)]
 [StringLength(2, 2)]
 [DataElement("156", typeof(X12_ID))]
 [Pos(2)]
 public string AmbulanceDropoffStateorProvinceCode_02 { get; set; }

Validation Order: 6

Only one of the EDI data elements at the specified positions and the annotated data element altogether must be not null.

Example: In N4 segment either AmbulanceDropoffStateorProvinceCode_02 or CountrySubdivisionCode_07 can exist but not both.

 

Paired

 [Paired(9)]
 [DataElement("66", typeof(X12_ID_66_3))]
 [Pos(8)]
 public string IdentificationCodeQualifier_08 { get; set; }

Validation Order: 5

If any of the elements at the specified positions or the annotated element is not null, then all the elements at the specified positions and the annotated element must be not null. It is applied to only one of all the paired items.

Example: In NM1 segment IdentificationCodeQualifier_08 is paired with IdentificationCode_09, e.g, if one of them exists then the other, must also exist.

 

RequiredAny

 [RequiredAny(3)]
 [StringLength(1, 50)]
 [DataElement("127", typeof(X12_AN))]
 [Pos(2)]
 public string ReferringProviderSecondaryIdentifier_02 { get; set; }

Validation Order: 7

At least one of the elements at the specified positions or the annotated element must be not null.

Example: In REF segment either ReferringProviderSecondaryIdentifier_02 or Description_03 must be provided.

 

RequiredIf

 [RequiredIf(1, ",B,C,G,J,Y,")]
 [DataElement("782", typeof(X12_R))]
[Pos(7)] public string BenefitAmount_07 { get; set; }

Validation Order: 8

When the element at the specified position is equal to one of the specified codes, then the annotated element must be not null.

In EB segment, BenefitAmount_07 is required when EligibilityorBenefitInformation_01 (the element at position 1) is "B,C,G,J, or Y".

 

ExclusionIf

 [ExclusionIf(1, ",A,")]
 [DataElement("782", typeof(X12_R))]
[Pos(7)] public string BenefitAmount_07 { get; set; }

Validation Order: 9

When the element at the specified position is equal to one of the specified codes, then the annotated element must be null.

In EB segment, BenefitAmount_07 must not be used when EligibilityorBenefitInformation_01 (the element at position 1) is "A".

 

Executing the EDI validation

To validate a .NET object that represents an EDI transaction, call the IsValid() method of EdiMessage, the base class for all EDI templates.

Internally, IsValid iterates through all items in the EDI transaction, e.g. all loops, segments, and elements, and matches them to the validation attributes configured in the EDI template.

In addition to the validation attributes, IsValid matches the reference number in the header and the trailer and checks the segment/message count in the trailers. Validation is usually executed when:

  • An EDI file is received from a trading partner, to ensure that only compliant EDI transactions are processed downstream.
  • An EDI file is about to be sent out to a trading partner, to ensure that the contents of the file are compliant with that partner's specification.

EDI trailers such as IEA, UNZ, etc. are automatically applied when POCOs are written out to an EDI file using EDI Writer, hence, the EDI trailer might have not been populated at the time IsValid is executed. To tell IsValid that this is the case, set SkipTrailerValidation to true.

  •  var ediStream = File.OpenRead(@"C:\edi.txt");
     List<IEdiItem> ediItems;
     using(var reader = new X12Reader(ediStream, "EdiFabric.Templates.X12"))
        ediItems = reader.ReadToEnd().ToList(); 
                                     
     var purchaseOrders = ediItems.OfType<TS850>();
    
     foreach (var purchaseOrder in purchaseOrders)
     {
        //  Validate
        MessageErrorContext errorContext;
        if (!purchaseOrder.IsValid(out errorContext))
        {
            //  Report it back to the sender, log, etc.
            var errors = errorContext.Flatten();
        }
        else
        {
            //  purchaseOrder is valid, handle it downstream
        }
     }
  • var ediStream = File.OpenRead(@"C:\edi.txt");
     List<IEdiItem> ediItems;
    using (var reader = new EdifactReader(ediStream, "EdiFabric.Templates.Edifact"))
    	ediItems = reader.ReadToEnd().ToList();
    
    var purchaseOrders = ediItems.OfType<TSORDERS>();
    
    foreach (var purchaseOrder in purchaseOrders)
    {
    	//  Validate using EDI codes map
    	MessageErrorContext errorContext;
    	if (!purchaseOrder.IsValid(out errorContext))
    	{
    		//  Report it back to the sender, log, etc.
    		var errors = errorContext.Flatten();
    	}
    	else
    	{
    		//  purchaseOrder is valid, handle it downstream
    	}
    }
  • var hl7Stream = File.OpenRead(@"C:\hl7.txt");
    
    List<IEdiItem> hl7Items;
    using (var reader = new Hl7Reader(hl7Stream, "EdiFabric.Templates.Hl7"))
    	hl7Items = reader.ReadToEnd().ToList();
    
    var dispenses = hl7Items.OfTypeList<TSRDSO13>();
    
    foreach (var dispense in dispenses)
    {
    	//  Validate
    	MessageErrorContext errorContext;
    	if (!dispense.IsValid(out errorContext))
    	{
    		//  Report it back to the sender, log, etc.
    		var errors = errorContext.Flatten();
    	}
    	else
    	{
    		//  dispense is valid, handle it downstream
    	}
    }
  • Stream ncpdpStream = File.OpenRead(@"C:\telco.txt");
    
    List<IEdiItem> ncpdpItems;
    using (var ncpdpReader = new NcpdpTelcoReader(ncpdpStream, "EdiFabric.Templates.Ncpdp"))
    	ncpdpItems = ncpdpReader.ReadToEnd().ToList();
    
    var claims = ncpdpItems.OfType<TSB1>();
    
    foreach (var claim in claims)
    {
    	//  Validate
    	MessageErrorContext errorContext;
    	if (!claim.IsValid(out errorContext))
    	{
    		//  Report it back to the sender, log, etc.
    		var errors = errorContext.Flatten();
    	}
    	else
    	{
    		//  claim is valid, handle it downstream
    	}
    }
  • Stream ncpdpStream = File.OpenRead(@"C:\script.txt");
    
    List<IEdiItem> ncpdpItems;
    using (var ncpdpReader = new NcpdpScriptReader(ncpdpStream, "EdiFabric.Templates.Ncpdp"))
    	ncpdpItems = ncpdpReader.ReadToEnd().ToList();
    
    var prescriptionRequests = ncpdpItems.OfType<TSNEWRX>();
    
    foreach (var prescriptionRequest in prescriptionRequests)
    {
    	//  Validate
    	MessageErrorContext errorContext;
    	if (!prescriptionRequest.IsValid(out errorContext))
    	{
    		//  Report it back to the sender, log, etc.
    		var errors = errorContext.Flatten();
    	}
    	else
    	{
    		//  prescription request is valid, handle it downstream
    	}
    }

Examples in GitHub:

 

The Validation Result

All validation errors are reported back in the form of a MessageErrorContext with detailed information for the position of the fault within the original EDI document, and the reason for the validation failure. IsValid returns true when no errors were found, and returns false when errors were found.

MessageErrorContext is the object returned as an out parameter of the IsValid() method only when there are any errors, otherwise, it is null. 

MessageErrorContext is the root object and maintains a collection of SegmentErrorContext, one for each segment with errors. SegmentErrorContext maintains a collection of DataElementErrorContext, one for each data element in that segment, that failed validation.

The architecture of the error contexts allows EdiFabric's AckMan to generate fully compliant EDI acknowledgments with the correct error codes.

 

Message Error Context

The MessageErrorContext contains the following details:

  • The transaction type, version, and control number of the validated object
  • The Codes collection containing any structural message level errors
  • The HasErrors property, which checks if either the Codes or the Errors have any items
  • The Errors collection containing all segment-specific issues (SegmentErrorContext) Should at least an item exist in this collection, MessageErrorCode.MessageWithErrors is added to the Codes collection
  • EDI message error codes

 

Segment Error Context

The SegmentErrorContext is a container for all the issues found in a segment and has the following identification properties;

  • Name, this is the segment tag, e.g. BIG or DTM
  • Value, this is the actual value of the segment from the original EDI document
  • SpecType, holds the type of the segment in the EDI template
  • Position, this is the position of the segment within its transaction, starting from ST or UNH which are at position 0

These properties should be sufficient to identify the whereabouts of any invalid segment in the original EDI document and to point to the corresponding class in the EDI template.

A SegmentErrorContext can contain multiple validation issues, for multiple data elements with errors for example. All the issues are listed under:

  • The Codes collection containing the segment-specific error codes
  • The Errors collection containing all data element-specific issues (DataElementErrorContext)
  • EDI segment error codes

 

DataElement Error Context

The DataElementErrorContext is a container for all the issues found for a particular data element and has the following identification properties:

  • Name, this is either the code defined the complex data element:

    edi composite element

    or the code defined for the simple data element:

    edi data element

  • Value, this is the actual value of the data element from the original EDI document
  • Position, this is the position of the complex or simple data element within the segment

    edi position

  • ComponentPosition is the position of the simple data element within the complex element. If the data element is part of a segment only, this is 0.

    edi composite position

  • RepetitionPosition, this is the position of the composite or simple repeating data element within the List

    edi repetitions

    These properties should be sufficient to identify the data elements that are invalid.

    The last property of the DataElementErrorContext is Code which points to the exact data element error code.

  • EDI data element error codes

 

User-Friendly Error Messages

The MessageErrorContext provides a Flatten method that lists all the errors in a single table, such as:

[BGM at pos 3] [4345 at pos 6 with value 123456789ABC] DataElementTooLong

Custom error messages are possible by creating custom attributes (inherited from any of the validation attributes) or applying custom validation.

 

How to extend EDI validation

There are two options to extend the validation in IsValid():

  1. By creating a custom validation attribute, inheriting from the base ValidationAttribute
  2. By implementing IEdiValidator

The inbuilt validation process will always invoke the validation of any attribute inheriting from ValidationAttribute whilst iterating through the items of the EDI transaction. It will then invoke any custom validation for any item implementing IEdiValidator.

 

Create custom validation attribute

Add custom validation code in the ValidateEdi method of IEdiValidator. The ValidationContext class contains the InstanceContext object, which holds the current object, its PropertyInfo, and its parent InstanceContext.

All indexes are relative to the current instance.

  • Create a new validation attribute

    [AttributeUsage(AttributeTargets.Property)]
    public class N1LoopValidationAttribute : ValidationAttribute
    {
        public N1LoopValidationAttribute() : base(10)
        {
        }
    
        public override SegmentErrorContext ValidateEdi(ValidationContext validationContext)
        {
    	var position = validationContext.SegmentIndex + 1;
    
    	var n1Loops = validationContext.InstanceContext.Instance as IList<Loop_N1_850>;
    	if (n1Loops != null)
    	{
    	    foreach (var n1Loop in n1Loops)
    	    {
    		//  Check if N1 exists and N2 also exist
    		if (n1Loop.N1 != null && n1Loop.N2 == null)
    			return new SegmentErrorContext("N2", validationContext.SegmentIndex + 2, null, GetType().GetTypeInfo(), SegmentErrorCode.RequiredSegmentMissing,
    				"N2 segment is missing.");
    
    		return null;
    	    }
    	}
    
    	return null;
        }
    }

    Apply the new validation attribute

    [Message("X12", "004010", "850")]
    public class TS850CustomValidation : TS850
    {
        [N1LoopValidation]
        [DataMember]
        [ListCount(200)]
        [Pos(34)]
        public new List<Loop_N1_850> N1Loop { get; set; }
    }
  • Create a new validation attribute

    [AttributeUsage(AttributeTargets.Property)]
    public class LinLoopValidationAttribute : ValidationAttribute
    {
        public LinLoopValidationAttribute() : base(10)
        {
        }
    
        public override SegmentErrorContext ValidateEdi(ValidationContext validationContext)
        {
    	var position = validationContext.SegmentIndex + 1;
    
    	var linLoops = validationContext.InstanceContext.Instance as IList<Loop_LIN_ORDERS>;
    	if (linLoops != null)
    	{
    	    foreach (var linLoop in linLoops)
    	    {
    		//  Count all existing segments before the one that is validated to apply the correct index
    		if (linLoop.PIA != null)
    			position += linLoop.PIA.Count;
    		if (linLoop.IMD != null)
    			position += linLoop.IMD.Count;
    		if (linLoop.MEA != null)
    			position += linLoop.MEA.Count;
    		if (linLoop.QTY != null)
    			position += linLoop.QTY.Count;
    		if (linLoop.PCD != null)
    			position += linLoop.PCD.Count;
    		if (linLoop.ALI != null)
    			position += linLoop.ALI.Count;
    
    		//  Check if QTY exists and DTM also exist
    		if (linLoop.QTY != null && linLoop.DTM == null)
    			return new SegmentErrorContext("DTM", position + 1, null,  GetType().GetTypeInfo(), SegmentErrorCode.RequiredSegmentMissing,
    				"DTM segment is missing.");
    	    }
    	}
    
    		return null;
        }
    }

    Apply the new validation attribute

    [Message("EDIFACT", "D96A", "ORDERS")]
    public class TSORDERSCustomValidation : TSORDERS
    {
        [LinLoopValidation]
        [DataMember]
        [ListCount(200000)]
        [Pos(21)]
        public new List<Loop_LIN_ORDERS> LINLoop { get; set; }
    }

Examples in GitHub:

 

Implement IEdiValidator

Implement IEdiValidator for any EDI loop, EDI segment, or EDI complex element, and add the situational validation logic in the ValidateEdi() method. This ensures that the logic will be executed as part of both IsValid() and AckMan. 

 public partial class Loop_2000A : IEdiValidator
 {
    public List<SegmentErrorContext> ValidateEdi(ValidationContext validationContext)
    {
        // Custom validation goes here, example below
        var result = new List<SegmentErrorContext>();
        
        if (N1 != null && N2 == null)
            result.Add(new SegmentErrorContext("N2", 
                validationContext.SegmentIndex + 2, GetType().GetTypeInfo(), 
                SegmentErrorCode.RequiredSegmentMissing,
                "N2 segment is missing."));

        return result;
    }
 }

 

Share this:

Was this article helpful?

Comments

0 comments

Please sign in to leave a comment.