Custom Route Programming
Adding Custom Step Code
T-Connect comes with pre-built steps on the portal: RECEIVE, PERSIST, SEND, VALIDATE. Developers can create custom steps, which may be incorporated into routes in the Portal. Some examples include: member lookups, edits and custom acknowledgement generation.
Here are the basic steps for setting up a custom step for your routes:
Duplicate $\Tctep\CustomSteps\EditStep.cs
Rename new file and class name
Add Logic
Add any relevant logging you wish to display step execution in the Portal. In this example, log activity describing persistence completion was added.
Below is a complete example of a custom step.
using System; using System.IO; using TctepConnector; using TctepRepository.Events; using TctepServiceExtensions.Routing; using TctepServiceExtensions.Steps; using Caladan.LinqToEdi.Parser; using Caladan.LinqToEdi.StandardEntities; using Caladan.X12Validation.Validators; using Caladan.TransactionEntities.X12_5010_837P; using System.Linq; using TctepRepository.Config; namespace CustomSteps { public partial class EditStep : StepBase, IStep { public override string GetStepName() => nameof(EditStep); public override StepOptions GetStepOptions() { return new StepOptions() { skipIndex = true, skipPersist = false }; } public EditStep(DependencyContainer container, CustomStepData input) : base(container, input) { } public override CustomStepData ExecuteStep() { var activeStream = Data.FileData.Data ?? throw new ArgumentException(nameof(Data.FileData.Data)); if (activeStream.CanSeek) activeStream.Position = 0; Type stType = null; X12HeaderInfo info = null; //Example of reading a configuration field from the step's route configuration of the Portal string connectionString = this.GetConfigValue( Data.RouteOperation, "ConnectionString", //name of the field "Data Source=.;Initial Catalog=MyDatabase;Integrated Security=True;" //default value if no field value is found ); //Always operate on a copy of the original stream using (var activeStreamCopy = new MemoryStream()) { activeStream.CopyTo(activeStreamCopy); activeStream.Position = 0; activeStreamCopy.Position = 0; using (var sr = new StreamReader(activeStreamCopy)) { //Read the ISA and GS segments info = X12HeaderInfo.FromStreamReader(sr, true); //Use the header to explicitly set the transaction type of the parsed file var resolver = X12TransactionSetResolver.Create(info); if (!resolver.TryResolveStType(out stType)) { throw new X12ParsingException($"Could not determine message type for this file (ST01: {info.ST01}, GS08: {info.GS08})"); } sr.BaseStream.Position = 0; //parse the stream and load the EDI POCO into memory var parser = new X12Parser(); var edi = parser.Parse(sr, stType, false); if (edi == null) throw new ConnectorException($"Could not parse edi file in {GetStepName()} EdiFileId: {Data.EdiFileId} and InterchangeId: {Data.InterchangeId}", true, AlertType.ProcessException); //an example of targeting a change for 837P transactions only if (stType.Name == "X837P_LoopST") { var loop2300 = edi.DescendantLoops().FirstOrDefault(); loop2300.CLM.CLM01 = System.Guid.NewGuid().ToString(); } //reset position of the stream copy sr.BaseStream.Position = 0; //replace the original stream with the stream copy edited above. edi.Save(activeStream, null, false); } //update the data payload of the current step activeStream.Position = 0; Data.FileData.Data = activeStream; } //save the updated step data and the updated transaction to the x12.tInboundEdi table of the TctepDb database Data.FileData.ParentEdiId = 0; this.PersistMessage(Data.FileData, EventCode.CusFileParsed, false, true); //write events that will be seen in the portal this.LogStepActivity($"Created 834 EDI with InterchangeId: {Data.FileData.InterchangeId} in {Sw.Elapsed.ToString()}"); this.LogStepActivity(EventCode.EditFinished); this.LogStepActivity($"Ran edit on message with EdiFileId: {Data.EdiFileId} and InterchangeId: {Data.InterchangeId} in {Sw.Elapsed.ToString()}"); this.LogStepActivity(EventCode.EditFinished); return Data; } } }
Adding Custom Form for Portal User Entry
Next step is to create and assign the template that will drive a the route configuration form in the T-Connect Portal. The first entry with ID 0 in the below merge statement can be used as a template. Below is a the minimum template that can be presented to the user. This will only prompt the user to enter the name of the custom step that was created.
'{"name":"Template","type":"operation","fields":[{"type":"text","name":"operationName","label":"Step Name","required":true,"data":"","sample":"Input a name"}]}',NULL,NULL,NULL,NULL)
All of the current templates can be found in the tFormTemplates, shown below:
MERGE INTO [portal].[tFormTemplates] AS Target USING (VALUES (0 ,'Default Template','tcDefaultOperationTemplate','{"name":null,"type":"operation","fields":[{"type":"text","name":"operationName","label":"Step Name","required":true,"data":"","sample":"Input a name"}]}',NULL,NULL,NULL,NULL) ,(1 ,'Persist Template','Persist Template','{"name":"Persist Message","type":"operation","fields":[{"type":"text","name":"operationName","label":"Step Name","required":true,"data":"","sample":"Input a name"}]}',NULL,NULL,NULL,NULL) ,(2 ,'Validation Template','Validation Template','{"name":"Validation","type":"operation","fields":[{"type":"text","name":"operationName","label":"Step Name","required":true,"data":"","sample":"Input a name"},{"type":"radio","name":"FailTransactionOnError","label":"Split invalid transactions","options":[{"id":false,"name":"Yes"},{"id":true,"name":"No"}],"required":true,"data":"false"}]}',NULL,NULL,NULL,NULL) -- receive locations ,(3 ,'File Receive Template','File Template', '{"name":"File","type":"receive","fields":[{"type":"text","name":"locationName","label":"Step Name","required":true,"data":"","sample":"Input a name"},{"type":"text","name":"AddressUri","label":"Address Uri","required":true,"data":"","sample":"C:\\path\\to\\folder\\"},{"type":"text","name":"extensions","label":"extensions","required":true,"data":"*.*","sample":"*.*"},{"type":"text","name":"pollinginterval","label":"polling interval (seconds)","required":true,"data":"10","sample":"10"}]}',NULL,NULL,NULL,NULL) ,(4 ,'SQL Receive Template','SQL Template', '{"name":"TConnect DB","type":"receive","fields":[{"type":"text","name":"locationName","label":"Step Name","required":true,"data":"","sample":"Input a name"},{"Name":"AddressUri","Type":"text","Label":"Connection String","Min":null,"Max":null,"Required":true,"Data":"Data Source=.;Initial Catalog=TConnectEDI837P;Integrated Security=true","Sample":"Data Source=ServerName;Initial Catalog=DatabaseName;Integrated Security=true;","Options":null},{"Name":"CommandText","Type":"text","Label":"Command Text","Min":null,"Max":null,"Required":true,"Data":"dbo.usp_GetOutboundBatch","Sample":"dbo.usp_GetOutboundBatch","Options":null},{"Name":"pollinginterval","Type":"text","Label":"polling interval (seconds)","Min":null,"Max":null,"Required":true,"Data":"120","Sample":"20","Options":null}]}',NULL,NULL,NULL,NULL) ,(5 ,'HTTP Receive Template','HTTP Template', '{"name":"HTTP","type":"receive","fields":[{"type":"text","name":"locationName","label":"Step Name","required":true,"data":"","sample":"Input a name"},{"type":"text","name":"AddressUri","label":"Address Uri","required":true,"data":"","sample":"http://www.example.com:port/path/"},{"type":"text","name":"extensions","label":"extensions","required":true,"data":"*.*","sample":"*.*"},{"type":"text","name":"pollinginterval","label":"polling interval (seconds)","required":true,"data":"10","sample":"10"}]}',NULL,NULL,NULL,NULL) ,(6 ,'SFTP Receive Template','SFTP Template', '{"name":"SFTP","type":"receive","fields":[{"type":"text","name":"locationName","label":"Step Name","required":true,"data":"","sample":"Input a name"},{"type":"text","name":"AddressUri","label":"Address Uri","required":true,"data":"","sample":"sftp://www.example.com:port/path/"},{"type":"text","name":"extensions","label":"extensions","required":true,"data":"*.*","sample":"*.*"},{"type":"text","name":"pollinginterval","label":"polling interval (seconds)","required":true,"data":"10","sample":"10"},{"type":"text","name":"username","label":"UserName","required":false,"data":"","sample":"username"},{"type":"password","name":"password","label":"Password","required":false,"data":"","sample":"password"}]}',NULL,NULL,NULL,NULL) -- send locations ,(7 ,'File Send Template','File Template', '{"name":"File","type":"send","fields":[{"type":"text","name":"locationName","label":"Step Name","required":true,"data":"","sample":"Input a name"},{"type":"select","name":"LocationContentTypeId","label":"Type of content","options":[{"id":"0","name":"Any"},{"id":"1","name":"Valid Transactions"},{"id":"2","name":"Invalid Transactions"},{"id":"3","name":"Functional Acknowledgment"},{"id":"4","name":"Claims Acknowledgement"},{"id":"5","name":"Validation Report"}],"required":true,"data":"0"},{"type":"text","name":"AddressUri","label":"Address Uri","required":true,"data":"","sample":"C:\\path\\to\\folder\\"},{"type":"radio","name":"overwrite","label":"Copy Mode","options":[{"id":true,"name":"Overwrite"},{"id":false,"name":"Create New"}],"required":true,"data":"true"}]}',NULL,NULL,NULL,NULL) ,(8 ,'SQL Send Template','SQL Template', '{"name":"SQL", "type":"send","fields":[{"type":"text","name":"locationName","label":"Step Name","required":true,"data":"","sample":"Input a name"},{"type":"select","name":"LocationContentTypeId","label":"Type of content","options":[{"id":"0","name":"Any"},{"id":"1","name":"Valid Transactions"},{"id":"2","name":"Invalid Transactions"},{"id":"3","name":"Functional Acknowledgment"},{"id":"4","name":"Claims Acknowledgement"},{"id":"5","name":"Validation Report"}],"required":true,"data":"0"},{"type":"text","name":"AddressUri","label":"Connection String","required":true,"data":"Data Source=.;Initial Catalog=TctepDb;Integrated Security=true;","sample":"Data Source=ServerName;Initial Catalog=DatabaseName;Integrated Security=true;"},{"Name":"CommandText","Type":"text","Label":"Command Text","Min":null,"Max":null,"Required":true,"Data":"[ext].[pAddOutbound]","Sample":"[ext].[pAddOutbound]","Options":null},{"Name":"@FileName","Type":"parameter","Label":"Parameter 1","Min":null,"Max":null,"Required":true,"Data":"FileName","Sample":"FileName","Options":null},{"Name":"@FileData","Type":"parameter","Label":"Parameter 1","Min":null,"Max":null,"Required":true,"Data":"FileData","Sample":"FileData","Options":null}]}',NULL,NULL,NULL,NULL) ,(9 ,'HTTP Send Template','HTTP Template', '{"name":"HTTP","type":"send","fields":[{"type":"text","name":"locationName","label":"Step Name","required":true,"data":"","sample":"Input a name"},{"type":"select","name":"LocationContentTypeId","label":"Type of content","options":[{"id":"0","name":"Any"},{"id":"1","name":"Valid Transactions"},{"id":"2","name":"Invalid Transactions"},{"id":"3","name":"Functional Acknowledgment"},{"id":"4","name":"Claims Acknowledgement"},{"id":"5","name":"Validation Report"}],"required":true,"data":"0"},{"type":"text","name":"AddressUri","label":"Address Uri","required":true,"data":"","sample":"http://www.example.com:port/path/"}]}',NULL,NULL,NULL,NULL) ,(10,'SFTP Send Template','SFTP Template', '{"name":"SFTP","type":"send","fields":[{"type":"text","name":"locationName","label":"Step Name","required":true,"data":"","sample":"Input a name"},{"type":"select","name":"LocationContentTypeId","label":"Type of content","options":[{"id":"0","name":"Any"},{"id":"1","name":"Valid Transactions"},{"id":"2","name":"Invalid Transactions"},{"id":"3","name":"Functional Acknowledgment"},{"id":"4","name":"Claims Acknowledgement"},{"id":"5","name":"Validation Report"}],"required":true,"data":"0"},{"type":"text","name":"AddressUri","label":"Address Uri","required":true,"data":"","sample":"sftp://www.example.com:port/path/"},{"type":"radio","name":"overwrite","label":"Copy Mode","options":[{"id":true,"name":"Overwrite"},{"id":false,"name":"Create New"}],"required":true,"data":"true"},{"type":"text","name":"username","label":"UserName","required":false,"data":"","sample":"username"},{"type":"password","name":"password","label":"Password","required":false,"data":"","sample":"password"}]}',NULL,NULL,NULL,NULL) -- More steps ,(11 ,'Edit Template','Edit Template','{"name":"Edit Message","type":"operation","fields":[{"type":"text","name":"operationName","label":"Step Name","required":true,"data":"","sample":"Input a name"}]}',NULL,NULL,NULL,NULL) ) AS Source ([FormTemplateId], [TemplateName],[TemplateDescription],[TemplateJson], [CreateDate],[CreateUser],[UpdateDate],[UpdateUser]) ON (Target.[FormTemplateId] = Source.[FormTemplateId]) WHEN MATCHED THEN UPDATE SET [TemplateName] = Source.[TemplateName], [TemplateDescription] = Source.[TemplateDescription], [TemplateJson] = Source.[TemplateJson], [FormTemplateId] = Source.[FormTemplateId], [CreateDate] = ISNULL(Source.[CreateDate], GETDATE()), [CreateUser] = ISNULL(Source.[CreateUser], SUSER_NAME()), [UpdateDate] = GETDATE(), [UpdateUser] = SUSER_NAME() WHEN NOT MATCHED BY TARGET THEN INSERT([FormTemplateId],[TemplateName],[TemplateDescription],[TemplateJson], [CreateDate],[CreateUser],[UpdateDate],[UpdateUser]) VALUES(Source.[FormTemplateId],Source.[TemplateName],Source.[TemplateDescription],Source.[TemplateJson], GETDATE(),SUSER_NAME(),GETDATE(),SUSER_NAME()) WHEN NOT MATCHED BY SOURCE THEN DELETE;
The ID of the row inserted into the tFormTemplate merge statement will be referenced in the next statement to be updated, tOperationType.
Adding Type to OperationType table
Add the custom step into the rt.tRouteOperationType merge statement. In this example, the custom step 'PersistStep' with ID 7 was added. If the custom step was created within the RouteExtensions project, then no assembly name is needed. However, an assembly name is required if an external visual studio project is used to create the custom step. The FormTemplateid value inserted in the previous step is also added.
MERGE INTO [rt].[tRouteOperationType] AS Target USING (VALUES (0,'UNKNOWN',NULL,NULL,NULL,NULL,'2018-05-03T15:57:44.103','WIN-7P0T8AAIQ4S\Administrator','2018-05-03T15:57:44.103','WIN-7P0T8AAIQ4S\Administrator') ,(1,'Validation','ValidationStep',NULL,NULL,2,'2018-05-03T15:57:44.103','WIN-7P0T8AAIQ4S\Administrator','2018-05-03T15:57:44.103','WIN-7P0T8AAIQ4S\Administrator') ,(2,'ID Injection','IDInjectionStep',NULL,NULL,NULL,'2018-05-03T15:57:44.103','WIN-7P0T8AAIQ4S\Administrator','2018-05-03T15:57:44.103','WIN-7P0T8AAIQ4S\Administrator') ,(3,'Enrichment','MockRoutingStep',NULL,NULL,NULL,'2018-05-03T15:57:44.103','WIN-7P0T8AAIQ4S\Administrator','2018-05-03T15:57:44.103','WIN-7P0T8AAIQ4S\Administrator') ,(4,'Member Lookup','MemberLookupStep',NULL,NULL,NULL,'2018-05-03T15:57:44.103','WIN-7P0T8AAIQ4S\Administrator','2018-05-03T15:57:44.103','WIN-7P0T8AAIQ4S\Administrator') ,(5,'Translate Flat File','ReceiveFlatFileStep',NULL,NULL,NULL,'2018-05-03T15:57:44.103','WIN-7P0T8AAIQ4S\Administrator','2018-05-03T15:57:44.103','WIN-7P0T8AAIQ4S\Administrator') ,(6,'Execute Map','MapStep',NULL,NULL,NULL,'2018-05-03T15:57:44.103','WIN-7P0T8AAIQ4S\Administrator','2018-05-03T15:57:44.103','WIN-7P0T8AAIQ4S\Administrator') ,(7,'Persist Message','PersistStep',NULL,NULL,1,NULL,NULL,NULL,NULL) --,(8,'Edit Message','EditStep','EditStep','CustomSteps, Version=1.0.0.0, Culture=neutral, PublicKeyToken=fa23e59c67c8098f, processorArchitecture=MSIL',11,NULL,NULL,NULL,NULL) ,(8,'Edit Message','EditStep','CustomSteps, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null',NULL,11,NULL,NULL,NULL,NULL) --,(9,'Test Default Form','PersistStep',NULL,NULL,NULL,NULL,NULL,NULL,NULL) ) AS Source ([OperationTypeId],[Name],[ClassName],[AssemblyName],[DefaultConfiguration],[FormTemplateId], [CreateDate],[CreateUser],[UpdateDate],[UpdateUser]) ON (Target.[OperationTypeId] = Source.[OperationTypeId]) WHEN MATCHED THEN UPDATE SET [Name] = Source.[Name], [ClassName] = Source.[ClassName], [AssemblyName] = Source.[AssemblyName], [DefaultConfiguration] = Source.[DefaultConfiguration], [FormTemplateId] = Source.[FormTemplateId], [CreateDate] = ISNULL(Source.[CreateDate], GETDATE()), [CreateUser] = ISNULL(Source.[CreateUser], SUSER_NAME()), [UpdateDate] = GETDATE(), [UpdateUser] = SUSER_NAME() WHEN NOT MATCHED BY TARGET THEN INSERT([OperationTypeId],[Name],[ClassName],[AssemblyName],[DefaultConfiguration],[FormTemplateId], [CreateDate],[CreateUser],[UpdateDate],[UpdateUser]) VALUES(Source.[OperationTypeId],Source.[Name],Source.[ClassName],Source.[AssemblyName],Source.[DefaultConfiguration],Source.[FormTemplateId], GETDATE(),SUSER_NAME(),GETDATE(),SUSER_NAME()) WHEN NOT MATCHED BY SOURCE THEN DELETE;
Configuration of Display Forms
There are several user controls that can be presented to a user when configuring a route step. These controls allow a developer to prompt a route author for settings that can then be read into the context of the custom step.
Below is an example popup:
In the above example, there are two fields for the user to populate:
- Step Name – A text box control
- Split Invalid Transactions – A radial control
Using the validation step as an example, the field for ‘split invalid transactions’ is represented by the following JSON configuration
{"name":"Validation","type":"operation","fields":[{"type":"text","name":"operationName","label":"Step Name","required":true,"data":"","sample":"Input a name"},{"type":"radio","name":"FailTransactionOnError","label":"Split invalid transactions","options":[{"id":false,"name":"Yes"},{"id":true,"name":"No"}],"required":true,"data":"false"}]}
Which re-arranged for readability is:
Name and Type on line 2 and 3 are top level fields that represent the entire popup. The ‘fields’ array contains all of the individual controls presented to the user.
Across all controls the following have the same meaning:
- Name: The name of the step, not shown to the user, but crucial as it is used as the key to retrieve the ‘data’ value within a custom step.
- Label: The name presented to the user within the popup control ex: STEP NAME
- Required: true or false. If true, form submission (save) is not allowed until the field is populated with a value.
- Data: The raw text that is saved to the database and retrievable within a custom step. Within a template, pre-populating the data field allows for the specification of a default value.
- Sample: Example text presented to the user if no value is populated.
- Options: for types radio or select will contain an enumeration of possible values. The ‘id’ is populated into the ‘data’ field when saved
For example, the same form without the required name entered will present the ‘sample’ text, ‘Input a name’ and will prevent submission of the form until a value is entered.
The below table details all possible user controls and their corresponding JSON configurations.
User Control Type |
JSON Configuration |
text |
{ "type": "text", "name": "locationName", "label": "Step Name", "required": true, "data": "", "sample": "Input a name" } |
Select |
{ "type": "select", "name": "LocationContentTypeId", "label": "Type of content", "options": [ { "id": "0", "name": "Any" }, { "id": "1", "name": "Valid Transactions" }, { "id": "2", "name": "Invalid Transactions" } ], "required": true, "data": "0" } |
Radio |
{ "type": "radio", "name": "overwrite", "label": "Copy Mode", "options": [ { "id": true, "name": "Overwrite" }, { "id": false, "name": "Create New" } ], "required": true, "data": "true" } |
checkbox |
{ "type": "checkbox", "name": "check1", "label": "Sample Checkbox", "options": [ { "id": 1, "name": "Opt1" }, { "id": 2, "name": "Opt2" }, { "id": 3, "name": "Opt3" } ], "required": true, "data": "" } |
Within a custom, it is possible to retrieve the entire contents of the field object as JSON by getting the configuration property of the RouteOperation field in StepBase. However, it is most useful to retrieve the value populated in the ‘Data’ field of the user control. This is accomplished within a custom step by using the StepBase.GetConfigValue method, as is shown below.
string failTransaction = this.GetConfigValue(Data.RouteOperation, Constants.FAIL_TRANSACTION_ON_ERROR, "False");
bool failOnError = false;
bool.TryParse(failTransaction, out failOnError);