Pipeline to fix the odd promotion of AS2To.
See StackOverflow – Why field is promoted as AS2ToInternal instead of AS2To

This C# pipeline code checks for that issues and fixes it by promoting the proper field. I never found out why it was happening and why it needed to be fixed.

using System;
using System.Linq;
using System.Xml;
using System.Text;
using System.Collections;
using System.Runtime.InteropServices;
using System.ComponentModel;
using System.IO;
using System.Resources;
using System.Reflection;
using System.Drawing;
using Microsoft.BizTalk.Message.Interop;
using Microsoft.BizTalk.Component.Interop;
using Microsoft.BizTalk.Component.Utilities;
using Microsoft.XLANGs.RuntimeTypes;
using Microsoft.XLANGs.BaseTypes;

namespace Microsoft.Samples.BizTalk.Pipelines.CustomComponent
{
	/// <summary>
	/// Pipeline component which can't be placed into any receive or send
	/// pipeline stage and do a property promotion / distinguished fields 
	/// writing based on arbitrary XPath.
	/// </summary>
	[ComponentCategory(CategoryTypes.CATID_PipelineComponent)]
	[ComponentCategory(CategoryTypes.CATID_Any)]
    [System.Runtime.InteropServices.Guid("EC9794D0-AE8F-42B9-A7E7-02A876998158")]

    public class SetAS2ToPromotedField : 
		BaseCustomTypeDescriptor,
		IBaseComponent, 
		Microsoft.BizTalk.Component.Interop.IComponent, 
		IPersistPropertyBag,
		IComponentUI
	{

        private string _Encoding = "ASCII"; 


        public SetAS2ToPromotedField()
            : base(resourceManager)
		{
		}

		#region Design-time property(ies)

        
        #endregion

		#region IBaseComponent Members

		/// <summary>
		/// Gets a component description.
		/// </summary>
		[Browsable(false)]
		public string Description
		{
            get 
            {
                return "SetAS2To from AS2ToInternal"; 
            }
		}

		/// <summary>
		/// Gets a component name.
		/// </summary>
		[Browsable(false)]
		public string Name
		{
            get 
            {
				return "SetAS2To";
            }
		}

		/// <summary>
		/// Gets a component version.
		/// </summary>
		[Browsable(false)]
		public string Version
		{
			get
			{
				return "1.0";
			}
		}

		#endregion

		#region IComponentUI Members

		/// <summary>
		/// Validate component.
		/// </summary>
		/// <param name="projectSystem">Project system</param>
		/// <returns>Enumerator of error message collection</returns>
		public IEnumerator Validate(object projectSystem)
		{
			// Don't do any validation
			return null;
		}

		/// <summary>
		/// Gets a component icon.
		/// </summary>
		[Browsable(false)]
		public System.IntPtr Icon
		{
			get
			{
				//return ((Bitmap) resourceManager.GetObject("CompIcon")).GetHicon();
                return new System.IntPtr(); 
			}
		}

		#endregion

		#region IComponent Members

		/// <summary>
		/// Executes a pipeline component.
		/// </summary>
		/// <param name="pContext">Pipeline context</param>
		/// <param name="pInMsg">Input message</param>
		/// <returns>Outgoing message</returns>
		public IBaseMessage Execute(IPipelineContext pContext, IBaseMessage pInMsg)
		{
			try
			{
				return ExecuteInternal(pContext, pInMsg);
			}
			catch(Exception e)
			{
				// Put component name as a source information in this exception,
				// so the event log in message could reflect this.
				e.Source = Name;
				throw e;
			}
		}

		#endregion

		#region IPersistPropertyBag Members

		/// <summary>
		/// Initialize component.
		/// </summary>
		public void InitNew()
		{
			// Do nothing
		}

		/// <summary>
		/// Get component class ID.
		/// </summary>
		/// <param name="classID"></param>
		public void GetClassID(out Guid classID)
		{
			classID = new Guid("1E7EC223-338E-4D4C-B275-FE9AD90ECE6C");
    }

		/// <summary>
		/// Load component properties from a property bag.
		/// </summary>
		/// <param name="propertyBag">Property bag</param>
		/// <param name="errorLog">Error log level</param>
		public void Load(IPropertyBag propertyBag, int errorLog)
		{
			if ( null == propertyBag )
				throw new ArgumentNullException("propertyBag");

            //string val = (string)ReadPropertyBag(pb, "XPath1");
            //if (val != null) _XPath1 = val;
            string val; 

            //val = (string)ReadPropertyBag(propertyBag, "FilenamePattern");
            //if (val != null) _FilenamePattern = val;
        }

		/// <summary>
		/// Save component properties to a property bag.
		/// </summary>
		/// <param name="propertyBag">Property bag</param>
		/// <param name="clearDirty">Clear dirty flag</param>
		/// <param name="saveAllProperties">Save all properties flag</param>
		public void Save(IPropertyBag propertyBag, bool clearDirty, bool saveAllProperties)
		{
			if ( null == propertyBag )
				throw new ArgumentNullException("propertyBag");

            //propertyBag.Write("FilenamePattern", _FilenamePattern);


		}

        #endregion

        #region Private Members

        /// <summary>
        /// Executes the logic for this component.
        /// </summary>
        /// <param name="pContext">Pipeline context</param>
        /// <param name="pInMsg">Input message</param>
        /// <returns>Outgoing message</returns>
        private IBaseMessage ExecuteInternal(IPipelineContext pContext, IBaseMessage pInMsg)
        {
            // Check arguments
            if (null == pContext)
                throw new ArgumentNullException("pContext");

            if (null == pInMsg)
                throw new ArgumentNullException("pInMsg");

            if (null == pInMsg.BodyPart)
                throw new ArgumentNullException("pInMsg.BodyPart");

            if (null == pInMsg.BodyPart.GetOriginalDataStream())
                throw new ArgumentNullException("pInMsg.BodyPart.GetOriginalDataStream()");


            //
            // Create a seekable read-only stream over the input message body part stream.
            //

            // Create a virtual stream, using GetOriginalDataStream() method on the IBaseMessagePart because
            // this call doesn't clone stream (instead of IBaseMessagePart.Data property).
            SeekableReadOnlyStream stream = new SeekableReadOnlyStream(pInMsg.BodyPart.GetOriginalDataStream());

            //
            // Create a new outgoing message, copy all required stuff.
            //

            // Create a new output message
            IBaseMessage outMessage = pContext.GetMessageFactory().CreateMessage();

            // Copy message context by reference
            outMessage.Context = pInMsg.Context;

            // Create new message body part
            IBaseMessagePart newBodyPart = pContext.GetMessageFactory().CreateMessagePart();

            // Copy body part properties by references.
            newBodyPart.PartProperties = pInMsg.BodyPart.PartProperties;

            // Neal added so we can change data in the message itself with replace commands
            StreamReader reader = new StreamReader(stream);
            string text = reader.ReadToEnd();

            byte[] byteArray;
            _Encoding = "ASCII";
            if (_Encoding == "UNICODE")
            {
                byteArray = System.Text.Encoding.Unicode.GetBytes(text);
            }
            else
            {
                byteArray = System.Text.Encoding.ASCII.GetBytes(text);
            }
            MemoryStream memStream = new MemoryStream(byteArray);



            string strAS2Namespace = "http://schemas.microsoft.com/BizTalk/2006/as2-properties";


            object objAS2To = pInMsg.Context.Read("AS2To", strAS2Namespace);
            string strAS2To = objAS2To != null ? objAS2To.ToString() : "";

            object objAS2From = pInMsg.Context.Read("AS2From", strAS2Namespace);
            string strAS2From = objAS2From != null ? objAS2From.ToString() : "";

            // Absolutely no idea why were are seeing the AS2To field promoted as AS2ToInternal. 
            // https://stackoverflow.com/questions/61464238/biztalk-as2-why-field-is-promoted-as-as2tointernal-instead-of-as2to 
            object objAS2ToInternal = pInMsg.Context.Read("AS2ToInternal", strAS2Namespace);
            string strAS2ToInternal = objAS2ToInternal != null ? objAS2ToInternal.ToString() : "";

            object objPreservedFileName = pInMsg.Context.Read("PreservedFileName", strAS2Namespace);
            string strPreservedFileName = objPreservedFileName != null ? objPreservedFileName.ToString() : "";

            string strHttpNamespace = "http://schemas.microsoft.com/BizTalk/2003/http-properties";
            object objHttpHeaders = pInMsg.Context.Read("InboundHttpHeaders", strHttpNamespace);
            string strHttpHeaders = objHttpHeaders != null ? objHttpHeaders.ToString() : "";

            // could not get debugger to work, so had to try Trace 
            string trcGUID = System.Guid.NewGuid().ToString();
            string trcData = "Filename=" + strPreservedFileName;
            string trcXML = "InboundHttpHeaders:" + strHttpHeaders;


            // If No AS2To found, then substitute the next best possible value by promoting it 
            string strNewAS2To = null;
            if (String.IsNullOrEmpty(strAS2To))
            {

                // NOTE: if these two don't work, we could also parse out values from the Httpheader 
                // (but not there, the value is AS2-To 
                if (!String.IsNullOrEmpty(strAS2ToInternal))
                {
                    strNewAS2To = strAS2ToInternal;
                }
                else
                {
                    throw new System.ApplicationException("Could not find any possible value for AS2To. " + 
                        " InboundHttpHeaders=" + strHttpHeaders 
                        );
                }

                // based on above logic, promote a new value to AS2To if needed 
                //pInMsg.Context.Write("AS2To", strAS2Namespace, strNewAS2To);
                pInMsg.Context.Promote("AS2To", strAS2Namespace, strNewAS2To);

            }


            // Set virtual stream as a data stream for the new message body part
            //newBodyPart.Data = stream;
            newBodyPart.Data = memStream;
            pContext.ResourceTracker.AddResource(memStream);

			// Copy message parts
			CopyMessageParts(pInMsg, outMessage, newBodyPart);

            //
            // Return outgoing message.
            //

            return outMessage;
		}



        public static long GetRandomNumberInRange(double minNumber, double maxNumber)
        {
            double dblRandomNumber = new Random().NextDouble() * (maxNumber - minNumber) + minNumber;
            long longRandomNumber = System.Convert.ToInt64(Math.Round(dblRandomNumber, 0));
            return longRandomNumber;
        }


		/// <summary>
		/// Copy message parts from source to destination message.
		/// </summary>
		/// <param name="sourceMessage">Source message</param>
		/// <param name="destinationMessage">Destination message</param>
		/// <param name="newBodyPart">New message body part</param>
		private void CopyMessageParts(IBaseMessage sourceMessage, IBaseMessage destinationMessage, IBaseMessagePart newBodyPart)
		{
			string bodyPartName = sourceMessage.BodyPartName;
			for (int c = 0; c < sourceMessage.PartCount; ++c)
			{
				string partName = null;
				IBaseMessagePart messagePart = sourceMessage.GetPartByIndex(c, out partName);
				if (partName != bodyPartName)
				{
					destinationMessage.AddPart(partName, messagePart, false);
				}
				else
				{
					destinationMessage.AddPart(bodyPartName, newBodyPart, true);
				}
			}
		}

		/// <summary>
		/// Gets a message type from the XML stream.
		/// </summary>
		/// <param name="stream">Seekable stream</param>
		/// <returns>Message type</returns>
		/// <exception cref="InvalidOperationException">If message type can't be determined</exception>
		private string GetMessageType(Stream stream)
		{
			// Fail if message stream is not seekable
			if (!stream.CanSeek)
				throw new InvalidOperationException("An attempt to get a message type for non-seekable stream");

			// Save the current stream position
			long position = stream.Position;
			try
			{
				// Create XmlTextReader over the message stream
				XmlTextReader reader = new XmlTextReader(stream);
				while (reader.Read())
				{
					// Check if current node in XmlTextReader is element and it's global
					if (XmlNodeType.Element == reader.NodeType && 0 == reader.Depth)
					{
						// Found a global element, now build message type as <namespaceURI>#<localName> 
						// if namespaceURI is not empty and <localName> if namespaceURI is empty.
						if (reader.NamespaceURI != null && reader.NamespaceURI.Length > 0)
						{
							return reader.NamespaceURI + '#' + reader.LocalName;
						}
						else
						{
							return reader.LocalName;
						}
					}
				}

				// Not found, fail
				throw new InvalidOperationException("Message type can't be determined");
			}
			finally
			{
				// Restore the stream position
				stream.Position = position;
			}
		}

		/// <summary>
		/// Read property bag in order to eat the argument exception which is thrown
		/// when properties are not populated.
		/// </summary>
		/// <param name="propertyBag">Property bag</param>
		/// <param name="propertyName">Property name</param>
		/// <returns>Property value</returns>
		private object ReadPropertyBag(IPropertyBag propertyBag, string propertyName)
		{
			object value;
			try
			{
				propertyBag.Read(propertyName, out value, 0);
				return value;
			}
			catch (ArgumentException)
			{
				// IPropertyBag.Read throws an ArgumentException if there are no properties in there.
				// Just return null in this case.
				return null;
			}
		}

		private SchemaList documentSchemaList = new SchemaList();

		private static PromotingMap promotingMap = new PromotingMap();
		private static ResourceManager resourceManager = new ResourceManager("Microsoft.Samples.BizTalk.Pipelines.CustomComponent.ArbitraryXPathPropertyHandler", Assembly.GetExecutingAssembly());
		private static PropertyBase messageTypeWrapper = new BTS.MessageType();

		//private const string DocumentSpecNamesPropertyName = "DocumentSpecNames";
		//private const string DocumentSpecNamespacesPropertyName = "DocumentSpecNamespaces";
		//private const char   DocumentSpecNamesSeparator = '|';
		
		#endregion
	}
}

Did you ever want to know how to process (parse) or create EDI files by just using .NET C#? There is a great library that can help.

I’m working on a new Udemy class that is an Intro to EDI (Electronic Data Interchange). Other than using more expensive enterprise systems like BizTalk, I wanted to show students how they could both 1) parse EDI themselves, and 2) use any good .NET libraries that are available.

In doing some research on StackOverflow, I ran across EDI.NET. The author, Constantinos Leftheris, from Greece, was inspired and influenced by the NewtonSoft JSON library. You may know that the NewtonSoft library allows you to serialize and deserialize JSON from objects in memory, to and from disk files (or strings).

With EDI.Net, you can do the same with EDI. The only catch is, you have to create a C# class (he calls it a POCO class) that defines the interchange, including all the EDI fields you want to access. It uses C# attributes (or decorators) to tell the software the relationship between an EDI column and the class variable.

It handles X12, EDIFact and the older TRADACOMS.

For example, if you are familiar with the X12 850 Purchase Order,
it a a “BEGIN” segment like this:

BEG*00*SA*1000012**20090827~

You interpret it by looking at an implementation guide, such as the one below:

In the example above, the "*" is the delimiter or separator character from each element. You then count over and each element has a sequential name, BEG01, BEG02, BEG03 (etc) based on the Segment Name "BEG" and a two digit number.

This tells us that BEG03 is the Purchase Order Number, and BEG05 is the PurchaseOrderDate. BEG04 is not used on a new PO, only for revisions to existing PO (which many companies do not support). Note that it has the letter "O" for Optional in the required column, while the other fields have "M" for Mandatory.

You can also learn from the above implementation guide that the PO Number (BEG03) can be from 1 to 22 alphanumeric (AN) characters.

The "type" for BEG05 is "DT" (Date) and it's min/max length are both 8. The comment tells us that is must in the format CCYYMMDD.

Note in the attributes below, that "BEG/2" is zero-based, representing BEG03. It uses COBOL-like "Picture" clauses to describe the length and type of number in the field.

The sample above has BEG01=SA, which is not in the implementation guide I included above. Each company will hvae its own implementation guide and they may vary slightly as to code values and other nuances. But certain things like the PO Number being in BEG03 and the PODate being in BEG05 should never change.

            [EdiValue(Path = "BEG/2", Description = "BEG03 - Purchase Order Number")]
            public string PurchaseOrderNumber { get; set; }

            [EdiValue("9(8)", Path = "BEG/4", Format = "yyyyMMdd", Description = "BEG05 - Purchase Order Date")]
            public string PurchaseOrderDate { get; set; }

Deserializing is the process of taking an EDI file and loading into an object made from the POCO class. To create a new EDI file, you would be the object in the C# program, then serialize it to disk as an EDI file.

Download the zip

To do the demo below, you will need either download the .zip file from the GitHub or do a Git Clone. You will be needing the the "POCO" class called "PurchaseOrder850" in X12_850.xs.

Instructions for an easy X12 demo

  1. Create new C# console program, put code below in the program.cs.
  2. From top menu, click "Tools", "NuGet Package Manager", "Package Manager Console", then type in "Install-Package "indice.Edi".
  3. In Solution Explorer, click "add" then "existing item", and select indice.Edi.Tests\Models\X12_850.cs (from the GitHub zip download).
  4. Adjust the filename to match where you put the files on your disk.
  5. Run it with the sample
  6. Then try it one of your EDI 850 files if you have one.
  7. Customize the C# POCO class as needed.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using indice.Edi;
using indice.Edi.Tests.Models;
using System.IO; 

namespace EDINetDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            string inputEDIFilename = @"c:\EDI.Net-master\test\indice.Edi.Tests\Samples\x12.850.edi";
            //string inputEDIFilename = @"c:\EDIClass\Samples\Sample_850_01_Orig.edi";

            var grammar = EdiGrammar.NewX12();
            grammar.SetAdvice(
                segmentNameDelimiter: '*',
                dataElementSeparator: '*',
                componentDataElementSeparator: '>',
                segmentTerminator: '~',
                releaseCharacter: null,
                reserved: null,
                decimalMark: '.');

            
            var po850 = default(PurchaseOrder_850);
            using (var stream = new StreamReader(inputEDIFilename))
            {
                po850 = new EdiSerializer().Deserialize<PurchaseOrder_850>(stream, grammar);

                // If you have only one ST and one PO/850 per file, 
                // you can use subscript 0, 
                // otherwise you will need loops here. 
                Console.WriteLine("PO Number:" + 
                  po850.Groups[0].Orders[0].PurchaseOrderNumber);
                Console.WriteLine("PO Date:" + 
                  po850.Groups[0].Orders[0].PurchaseOrderDate);

                foreach (var lineitem in po850.Groups[0].Orders[0].Items)
                {
                    Console.WriteLine(" LineItem:");
                    Console.WriteLine("  ItemNum=" + lineitem.OrderLineNumber);
                    Console.WriteLine("  Qty=" + lineitem.QuantityOrdered);
                    Console.WriteLine("  Price=" + lineitem.UnitPrice);
                    Console.WriteLine("  PartNo=" + lineitem.BuyersPartno);
                    Console.WriteLine("  Descr=" + lineitem.ProductDescription);
                }
                // 1) store PO into Database
                //    (create SQL statements or call Stored Proc) 
                // 2) write to XML for ERP system 
                // 3) call some web service 


            }

            Console.WriteLine("\n\n Press enter to end:");
            Console.ReadLine();
        }
    }
}

Today, I was driven crazy by the Pipeline Editor in Visual Studio. I dropped two different pipeline components in it, then when I closed and re-opened it they would show the same name.

When I opened the .btp file in a text editor it looked okay. So it was apparently a problem displaying the user interface. Also, the run time was having a problem, as the pipeline component I was trying to test wasn’t being executed. My guess it was running the first one twice.

Finally, I found my issue. Yes, it was my fault!

I copied a pipeline line project from an existing one to a new one. I changed the GUID, but I failed to change the class name (and the namespace was identical). So the pipeline editor gets very confused in this scenario.

I had “public class ArbitraryXPathPropertyHandlerComp” in both C# pipeline components. I changed each to have names specific to their functions (as shown below, the name “SetAS2ToPromotedField”).


public class SetAS2ToPromotedField :
BaseCustomTypeDescriptor,
IBaseComponent,
Microsoft.BizTalk.Component.Interop.IComponent,
IPersistPropertyBag,
IComponentUI
{

In BizTalk, you can export and import and export bindings to/from the BizTalk Database using the BizTalk Administration Console and/or BTSTask.exe (command line utility).

The binding file has five major sections:

1) ModuleRefCollection (Orchestrations a.k.a. “Services” and Orchestration bindings)
2) SendPortCollection (Send Ports)
3) DistributionListCollection (Send Port Groups)
4) ReceivePortCollection (Receive Port and their related Receive Locations)
5) PartCollection (Trading Partner Management [TPM] Parties)

Structure of Binding File – Top-Most Elements (Sections)

<?xml version="1.0" encoding="utf-8"?>
<BindingInfo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" Assembly="Microsoft.BizTalk.Deployment, Version=3.0.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" Version="3.5.1.0" BindingStatus="FullyBound" BoundEndpoints="18" TotalEndpoints="18">

  <Timestamp>2020-03-30T10:19:30.8927543-05:00</Timestamp>
  
  <ModuleRefCollection>
     <TrackedSchemas...
     <Service...
  </ModuleRefCollection>
  
  <SendPortCollection>
  </SendPortCollection>
  
  <DistributionListCollection />
  
  <ReceivePortCollection>
  </ReceivePortCollection>
  
  <PartyCollection xsi:nil="true" />

</BindingInfo>

Creating the Binding File

The normal process is to go to an application in BizTalk Admin Console, right click, and select “Export”, then “Bindings…”. You will be prompted for a file name (the directory defaults to: C:\Users\YourUser\AppData\Local\BizTalk\Binding Files\, but you can change it). By default, the filename is the name of your application followed by “_Bindinginfo.xml”.

So for example, for the Application called “Lesson1”, then binding file by default would be:
C:\Users\YourUser\AppData\Local\BizTalk\Binding Files\Lesson1.BindingInfo.xml

Editing the binding file

You can open the binding file in NotePad++ or your favorite editor. It’s just XML, don’t be afraid of it.
For example, when you deploy from test to production environments, you often need to change directory names, SQL Server/Database Names, and/or WebSite URLs.

Duplicating a SendPort

To duplicate a Send Port, you can export the bindings for the app, remove everything except the root element, the and the one you want to duplicate. Change the name of the SendPort, and save the file. You can use an XML plugin to NotePad++ to make sure the file is still standard XML. Usually the URI (disk path or URL) will be changed as well. Then import back into the same application, and voila – your Send Port is copied or duplicated.

You can do similarly for Receive Ports, but note two things:
1) A Receive Port can have multiple ReceiveLocations, and Receive Locations always exist “under” the Receive Port.
2) The Receive Locations URI must be unique (change it before you import it).

Example of a Send Port Binding

    <SendPort Name="sp_Lesson1_Outbound" IsStatic="true" IsTwoWay="false" BindingOption="0" AnalyticsEnabled="false">
      <Description xsi:nil="true" />
      <TransmitPipeline Name="Microsoft.BizTalk.DefaultPipelines.PassThruTransmit" FullyQualifiedName="Microsoft.BizTalk.DefaultPipelines.PassThruTransmit, Microsoft.BizTalk.DefaultPipelines, Version=3.0.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" Type="2" TrackingOption="None" Description="" />
      <PrimaryTransport>
        <Address>c:\BizTalk\Lesson1\Outbound\%MessageID%_%SourceFileName%</Address>
        <TransportType Name="FILE" Capabilities="11" ConfigurationClsid="5e49e3a6-b4fc-4077-b44c-22f34a242fdb" />
        <TransportTypeData>&lt;CustomProps&gt;&lt;UseTempFileOnWrite vt="11"&gt;0&lt;/UseTempFileOnWrite&gt;&lt;AllowCacheOnWrite vt="11"&gt;0&lt;/AllowCacheOnWrite&gt;&lt;CopyMode vt="19"&gt;1&lt;/CopyMode&gt;&lt;FileName vt="8"&gt;%MessageID%_%SourceFileName%&lt;/FileName&gt;&lt;/CustomProps&gt;</TransportTypeData>
        <RetryCount>3</RetryCount>
        <RetryInterval>1</RetryInterval>
        <ServiceWindowEnabled>false</ServiceWindowEnabled>
        <FromTime>2020-02-09T08:00:00</FromTime>
        <ToTime>2020-02-10T07:59:59</ToTime>
        <Primary>true</Primary>
        <OrderedDelivery>false</OrderedDelivery>
        <DeliveryNotification>1</DeliveryNotification>
        <SendHandler Name="BizTalkServerApplication" HostTrusted="false">
          <TransportType Name="FILE" Capabilities="11" ConfigurationClsid="5e49e3a6-b4fc-4077-b44c-22f34a242fdb" />
        </SendHandler>
      </PrimaryTransport>
      <SecondaryTransport>
        <Address />
        <RetryCount>3</RetryCount>
        <RetryInterval>5</RetryInterval>
        <ServiceWindowEnabled>false</ServiceWindowEnabled>
        <FromTime>2020-02-09T08:00:00</FromTime>
        <ToTime>2020-02-10T07:59:59</ToTime>
        <Primary>false</Primary>
        <OrderedDelivery>false</OrderedDelivery>
        <DeliveryNotification>1</DeliveryNotification>
        <SendHandler xsi:nil="true" />
      </SecondaryTransport>
      <ReceivePipelineData xsi:nil="true" />
      <Tracking>204</Tracking>
      <Filter>&lt;?xml version="1.0" encoding="utf-16"?&gt;
&lt;Filter xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"&gt;
  &lt;Group&gt;
    &lt;Statement Property="BTS.ReceivePortName" Operator="0" Value="rp_Lesson1_Inbound" /&gt;
  &lt;/Group&gt;
&lt;/Filter&gt;</Filter>
      <Transforms />
      <OrderedDelivery>false</OrderedDelivery>
      <Priority>5</Priority>
      <StopSendingOnFailure>false</StopSendingOnFailure>
      <RouteFailedMessage>false</RouteFailedMessage>
      <ApplicationName>Lesson1</ApplicationName>
    </SendPort>

The trickiest part of the above is this:

        <TransportTypeData>&lt;CustomProps&gt;&lt;UseTempFileOnWrite vt="11"&gt;0&lt;/UseTempFileOnWrite&gt;&lt;AllowCacheOnWrite vt="11"&gt;0&lt;/AllowCacheOnWrite&gt;&lt;CopyMode vt="19"&gt;1&lt;/CopyMode&gt;&lt;FileName vt="8"&gt;%MessageID%_%SourceFileName%&lt;/FileName&gt;&lt;/CustomProps&gt;</TransportTypeData>

This is sometimes called encoding. The value of is actually more XML, but it is different depending on what adapter you use in your Send Port. So BizTalk encodes that XML with something like URLEncoding.

To make it more readable, you can do two change all commands in NotePad++:
1) Change > to >
2) Change < to < However I do not recommend going the other direction, there may be some conflicts. After that, and formatting it, it looks like this:

<TransportTypeData>
	<CustomProps>
		<UseTempFileOnWrite vt="11">0</UseTempFileOnWrite>
		<AllowCacheOnWrite vt="11">0</AllowCacheOnWrite>
		<CopyMode vt="19">1</CopyMode>
		<FileName vt="8">%MessageID%_%SourceFileName%</FileName>
	</CustomProps>
</TransportTypeData>

Send Ports for WCF and SQL are much more complicated then the simple one below, as they can have a dozen or more parameters.

The “Filter” section is also encoded:

      <Filter>&lt;?xml version="1.0" encoding="utf-16"?&gt;
&lt;Filter xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"&gt;
  &lt;Group&gt;
    &lt;Statement Property="BTS.ReceivePortName" Operator="0" Value="rp_Lesson1_Inbound" /&gt;
  &lt;/Group&gt;
&lt;/Filter&gt;</Filter>

Unencoded, it looks like this:

      <Filter><?xml version="1.0" encoding="utf-16"?>
<Filter xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Group>
    <Statement Property="BTS.ReceivePortName" Operator="0" Value="rp_Lesson1_Inbound" />
  </Group>
</Filter></Filter>

This shows you that the Send Port has a subscription (filter) on BTS.ReceivePortName = "rp_Lesson1_Inbound". The "equal" sign is encoded into the Operator=0 statement. I don't have an quick listing of the possible values of the Operator attribute.

The Full Error Was

The message could not be sent to the SMTP server. The transport error code was 0x80040217

Solution

I had to go to the BizTalk Adapters in BizTalk Admin Console, find the SMTP adapter, then configure it for the host.
On the properties window, there is an “Authentication Type” which defaulted to “NTLM Authentication”.

You may need to talk to your infrastructure/email team to see what is required by your email server. You might need to provide a basic authentication userid/password here.

You can optionally specify your email server name here, and default “reply-to email address”, rather having to specify it in every single SMTP Send Port.

Alternatively

C# to Send Email

The C# Mail.sendEmail method below can be called from a BizTalk Orchestration to send an email.
You can also write a wrapper test method to call it. When I got the error above, I checked this C# first, and it was able to send an email, so I knew the issue was in BizTalk.

The routine below is bare-bones as simple as can be. It doesn’t handle authentication or attachments or HTML bodies. So it could be enhanced with more parms to handle those types of scennarios.

Alternatively, you could try turning off or changing the authentication in the Send Port itself, but several other issues and blogs I found suggested that you have to setup the adapter first, before trying any overrides on the Send Port.


    public class Mail
    {

        // Example Call: 
          ABC.Helpers.Mail.sendEmail("nrwalters@abc.com", "EmailTester C#", "This is the message body"); 

        //send an email alert
        public static void sendEmail(string recipient, string subject, string message)
        {
            try
            {
                string from = "noreply@abc.com";
                //string host = "smtp.abc.com";
                int smtpPort = 25;

                //append the system name to the email
                string systemEnvironmentName = Config.getConfigValue("systemName");
                subject = subject.Trim() + " [" + systemEnvironmentName.Trim() + "]";

                MailMessage mail = new MailMessage();
                SmtpClient client = new SmtpClient();

                //set the email address from address
                MailAddress fromEmail = new MailAddress(from);
                mail.From = fromEmail;

                //split the recipient email address by ';' and add
                foreach (var address in recipient.Split(new[] { ";" }, StringSplitOptions.RemoveEmptyEntries))
                {
                    mail.To.Add(address);
                }



                client.Port = smtpPort;
                client.DeliveryMethod = SmtpDeliveryMethod.Network;
                client.UseDefaultCredentials = false;
                client.Host = host;
                mail.Subject = subject;
                mail.Body = message;
                mail.IsBodyHtml = false;

                client.Send(mail);
            }
            catch (SmtpException ex)
            {
                LoggingToFiles.logHelperError("sendEmail", ex.Message);
                throw new Exception("SMTP.Exception in function sendEmail: " + ex.Message);
            }
            catch (Exception ex)
            {
                LoggingToFiles.logHelperError("sendEmail", ex.Message);
                throw new Exception("Exception in function sendEmail: " + ex.Message);
            }
        }

    }

I got the same error as the one in this blog:
http://brucetank.blogspot.com/2017/10/biztalk-2016-wcf-custom-transport.html


System.Configuration.ConfigurationErrorsException: Unrecognized attribute 'ApplicationName'. Note that attribute names are case-sensitive.

The difference is that I got it run-time, where in the blog above, he got it at the time of importing the bindings.

I was afraid that maybe the Cumulative Updates (CUs) were out of sync; and I have a project underway to make sure that both systems are on the latest CU6.

I tried removing the “ApplicationName = ”” out of the binding file and re-importing, but when I went to BizTalk Admin in my Production environment, the Application Name was there in the properties on the WCF-Custom (SQL) send port (and still had a blank value).

I looked for other differences between test and production, and discovered that I had switched it to a 64-bit Host Instance in Prod. When I switched it back to 32-bit Host Instance, it got past this issue.

What I did to cause the error

Dropped an XML file that went through a Map, and was picked up by a Send Port with a CSV Pipeline.
Worked okay if I used PassThru Pipeline.

Suspend Erorr

There was a failure executing the send pipeline: “DL.MGLocationData.Pipelines.MyAppNameLocationDataCSV_Send, DL.MGLocationData.Pipelines, Version=1.0.0.0, Culture=neutral, PublicKeyToken=59a32f7375d7234a”
Source: “Flat file assembler” Send Port: “sp_MyAppNameLocationData_CSV” URI: “e:\Integration\Outbound\MyAppNameLocationData\MyAppName_%datetime%.csv” Reason: Object reference not set to an instance of an object

Solution

Turn on output map validation for your map.

Check the following:
1) Your are mapping something to every single CSV column
2) You don’t have same column names misspelled (one way in map, and another way in the schema)

Here’s an example where I wanted to create a small XML file with a unique list of TerminalIDs from my application.

On the left, which I had to blur out, the Looping functoid is connected to a parent record that repeats, and the other lines are connect to the source of the TerminalID.

The scripting component has the following “Inline C#” code:

public System.Collections.Generic.List<string> duplicateList = new System.Collections.Generic.List<string>();

public bool IsDuplicate( string terminalID )
{
     if( duplicateList.Contains( terminalID) )
        return true;
     duplicateList.Add( terminalID );
     return false;
}

You can see that it creates a collection object called a "Generic.List". It then adds terminalIDs to that list, after checking if the item is already in the list or not. It then returns a true or false.

The logical equals functoid tests the output of the above to "false". I think it is used to make sure we send a boolean true/false to the item on the right side of the map. Maybe it could be avoided, but I saw this in someone else's map, and I left it for now. I made one attempt to remove it and reverse the true/false coming out of Scripting Component, but it didn't work.

In case you didn't know, when set a field on the right side of the map to "false", then that item doesn't get mapped out. You basically suppress it.

The output, when I passed two inputs with "ABC" as the terminal ID is as follows:

<?xml version="1.0"?>
<ns0:UtilListTerminalIDs xmlns:ns0="http://TMW.Integration.Schemas.UtilListTerminalIDs">
  <UtilListTerminalID>
     <TerminalID>ABC</TerminalID>
  </UtilListTerminalID>
</ns0:UtilListTerminalIDs>

Remember that you can right-click the map, and click "Validate" to see the XSLT behind the map. The XSLT file will be shown in the Output window, and you can do a CNTL-Click on the .XSL filename.

Later, I passed two messages to the following C# routine which identifies if a TerminalID was not found in the target database (after doing a query on it). I could have also done the dup logic in the C# routine. This code uses XPath and the XmlDoc.SelectingSingleNode method to lookup the terminal ID in the results of the query. (The database query was previously returned as an XML message in the orchestration.)

I didn't want the orchestration to continue, because it would get an error later when it tries to insert a row into another table that requires the TerminalID to be in the target database.

        public static string FindMissingTerminalIDsInTRMSTResponse(
XmlDocument UtilListTerminalIDs, XmlDocument TRMSResponse)
{
string rt = "";
string xpathTerminalIDs = "//TerminalID";
XmlNodeList terminalNodes = UtilListTerminalIDs.SelectNodes(xpathTerminalIDs);
int countMatches = 0;
int countNodes = 0;
int countMisMatches = 0;

foreach (XmlNode terminalNode in terminalNodes)
{
countNodes++;
string xpathVerify = "//*[local-name()='TSATRM'][text()='" + terminalNode.InnerText + "']";
Console.WriteLine("xpathVerify=" + xpathVerify);

XmlNode testTerminalExistsNode = TRMSResponse.SelectSingleNode(xpathVerify);

if (testTerminalExistsNode != null)
{
countMatches++;
}
else
{
countMisMatches++;
if (countMisMatches > 1)
{
rt = rt + ", "; // comma separate after the first one
}
rt = rt + terminalNode.InnerText;
}

}

At first, I was going to return a boolean from this method, but then I decided that I would suspend the orchestration if the input had missing TerminalIDs, and the message should list all the missing IDs in a string. That went into my orchestration Suspend Shape.