Thursday, 6 June 2024

PGP Encryption and Digital signature in Dynamics 365 Finance and Operations

In this blog post I am going to discuss about Digital signature and PGP encryption for txt/xml data. This is generally required for payment processing files being sent out of D365 FO. 

As a pre-requisite, you must have a PGP encrypted public key in format as

-----BEGIN PGP PUBLIC KEY BLOCK-----

key content

-----END PGP PUBLIC KEY BLOCK-----


, a private key (if you don't have, then generate it using kleopatra tool) in format as 

-----BEGIN PGP ARMORED FILE-----

key content

-----END PGP ARMORED FILE-----


and a passphrase for private key. 

These keys and passphrase must be securely stored. I have stored it over Azure key vault and is using D365 Key vault parameters to retrieve that using azure client app. (for Dev box, Same is possible via txt files stored locally too, Or as a resource file for Dev and other environments, Or as a txt file over azure storage account).

Although its the safest way to use azure key vault, but there is a minor problem as to how the whole key is stored. Generally key must include special characters '\r\n' (Not directly visible unless read via code or using tools such as notepad++), when pasting it over key vault it ignores the new lines and considers whole data as a single line. To overcome this, do use the line ending characters before pasting it to key vault as

-----BEGIN PGP PUBLIC KEY BLOCK-----\r\nVersion: PGP Command Line v10.1.0 (Build 52) (Linux)\r\n\r\nkey content\r\n-----END PGP PUBLIC KEY BLOCK----

Once you are finalized with all these, lets proceed for code. 

We will start with the PGP helper class/

 

using Org.BouncyCastle.Bcpg.OpenPgp;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.Bcpg;

class PGPHelper
{
    public System.IO.Stream EncryptFile(System.IO.Stream inputFile, Str publicKeyString)
    {
        PgpPublicKey publicKey = this.ReadPublicKey(publicKeyString);
        System.IO.Stream outputStream = new System.IO.MemoryStream();

        PgpEncryptedDataGenerator encryptedData = new PgpEncryptedDataGenerator(Org.BouncyCastle.Bcpg.SymmetricKeyAlgorithmTag::Cast5, true, new SecureRandom());
        encryptedData.AddMethod(publicKey);

        int bufferSize = 1 << 16;
        ArmoredOutputStream armoredStream = new ArmoredOutputStream(outputStream);
        System.IO.Stream outStream = encryptedData.Open(armoredStream, bufferSize);

        //PgpCompressedDataGenerator compressedData = new PgpCompressedDataGenerator(Org.BouncyCastle.Bcpg.CompressionAlgorithmTag::Uncompressed);
        //var compressedOut = compressedData.Open(outStream);

        System.Byte[] buffer = new System.Byte[bufferSize]();
        int len = inputFile.Read(buffer, 0, bufferSize);
        if (len > 0)
        {
            outStream.write(buffer, 0, len);
        }

        armoredStream.Close();
        outputStream.Position = 0;
        return outputStream;
    }

    public System.IO.Stream SignFile(System.IO.Stream inputFile, Str privateKeyString, Str passphrase)
    {
        PgpPrivateKey privateKey = this.ReadPrivateKey(privateKeyString, passphrase);
        System.IO.Stream outputStream = new System.IO.MemoryStream();
        PgpSignatureGenerator pgpSignatureGenerator = new PgpSignatureGenerator(privateKey.PublicKeyPacket.Algorithm, Org.BouncyCastle.Bcpg.HashAlgorithmTag::Sha256);
        pgpSignatureGenerator.InitSign(0, privateKey);

        PgpLiteralDataGenerator literalData = new PgpLiteralDataGenerator();
        System.Char format = Global::toanytype('b');
        var literalOut = literalData.Open(outputStream, format, "filename", inputFile.Length, DateTimeUtil::utcNow());

        var signatureOut = pgpSignatureGenerator.GenerateOnePassVersion(false);
        signatureOut.Encode(literalOut);

        inputFile.CopyTo(literalOut);

        var signature = pgpSignatureGenerator.Generate();
        signature.Encode(literalOut);

        literalOut.Close();
        outputStream.Position = 0;

        return outputStream;
    }

    private PgpPublicKey ReadPublicKey(Str publicKeyString)
    {
        System.Byte[] getpublicKeyByte = System.Text.Encoding::UTF8.GetBytes(publicKeyString);

        System.IO.Stream publicKeyStream = new System.IO.MemoryStream(getpublicKeyByte);
        System.IO.Stream decodedStream  = PgpUtilities::GetDecoderStream(publicKeyStream);
        PgpPublicKeyRingBundle pgpPub = new PgpPublicKeyRingBundle(decodedStream);
        PgpPublicKeyRing kRing;
        System.Collections.IEnumerator kRingenumerator = pgpPub.GetKeyRings().GetEnumerator();
        while (kRingenumerator.MoveNext())
        {
            kRing = kRingenumerator.Current;
            PgpPublicKey k;
            System.Collections.IEnumerator kenumerator = kRing.GetPublicKeys().GetEnumerator();
            while (kenumerator.MoveNext())
            {
                k = kenumerator.Current;
                if (k.IsEncryptionKey)
                {
                    return k;
                }
            }
        }

        throw error('No encryption key found in public key ring.');
    }

    private PgpPrivateKey ReadPrivateKey(Str privateKeyString, Str passphrase)
    {
        System.Byte[] getprivateKeyByte = System.Text.Encoding::UTF8.GetBytes(privateKeyString);
        System.Byte[] getpassphraseByte = System.Text.Encoding::UTF8.GetBytes(passphrase);

        System.IO.Stream privateKeyStream = new System.IO.MemoryStream(getprivateKeyByte);
        System.IO.Stream decodedStream = PgpUtilities::GetDecoderStream(privateKeyStream);
        PgpSecretKeyRingBundle pgpSec = new PgpSecretKeyRingBundle(decodedStream);

        PgpSecretKey secretKey;
        PgpSecretKeyRing kRing;
        System.Collections.IEnumerator kRingenumerator = pgpSec.GetKeyRings().GetEnumerator();
        while (kRingenumerator.MoveNext())
        {
            kRing = kRingenumerator.Current;

            PgpSecretKey k;
            System.Collections.IEnumerator kenumerator = kRing.GetSecretKeys().GetEnumerator();
            while (kenumerator.MoveNext())
            {
                k = kenumerator.Current;
                if (k.IsSigningKey)
                {
                    secretKey = k;
                    break;
                }
            }

            if (secretKey != null)
            {
                break;
            }
        }

        if (secretKey == null)
        {
            throw error('No signing key found in secret key ring.');
        }
        
        System.Char[] passPhraseChar = System.Text.Encoding::UTF8.GetChars(getpassphraseByte);
        PgpPrivateKey privateKey = secretKey.ExtractPrivateKey(passPhraseChar);
        return privateKey;
    }

}

Code can be modified if compression is needed. (I have it commented, just uncomment it if needed).
Next to use the helper class to do the actual encryption and signature. 

We can read the file contents to be encrypted or use the data generated at runtime using io.stream.


internal final class RunnableClass1
{
	public static void main(Args _args)
    {
        KeyVaultParameters          keyVaultParameters;
                KeyVaultCertificateTable    certTable;
                //FinTechDotNetLib.PgpHelper pgpHelper = new FinTechDotNetLib.PgpHelper();
                PGPHelper   pgpHelper = new PGPHelper();
                System.IO.Stream encryptedStream;
                System.IO.Stream signedStream;
                
         select keyVaultParameters
            where  keyVaultParameters.Name = 'keyVaulttest';
            
            System.IO.Stream streamName = new System.IO.MemoryStream(System.Text.Encoding::UTF8.GetBytes(System.IO.File::ReadAllText(@"D:\Test.xml")));

                if (keyVaultParameters.DigitalSignature) //This is custom No/Yes field
                {
                    str privateKey = KeyVaultCertificateHelper::getManualSecretValue(KeyVaultCertificateTable::findByName('privatekey').RecId);
                    str formattedPrivateKey;
                    formattedPrivateKey = strReplace(privateKey, "\\r\\n", '\r\n');

                    signedStream = pgpHelper.SignFile(streamName, formattedPrivateKey, KeyVaultCertificateHelper::getManualSecretValue(KeyVaultCertificateTable::findByName('passphrase').RecId));
                }
                if (keyVaultParameters.DigitalSignature && keyVaultParameters.Encryption) //These are custom No/Yes field
                {
                    str publicKey = KeyVaultCertificateHelper::getManualSecretValue(KeyVaultCertificateTable::findByName('publickey').RecId);
                    str formattedPublicKey;
                    formattedPublicKey = strReplace(publicKey, "\\r\\n", '\r\n');
                    encryptedStream = pgpHelper.EncryptFile(signedStream, formattedPublicKey);
                }
                else if (keyVaultParameters.Encryption && !keyVaultParameters.DigitalSignature) //These are custom No/Yes field
                {
                    str publicKey = KeyVaultCertificateHelper::getManualSecretValue(KeyVaultCertificateTable::findByName('publickey').RecId);
                    str formattedPublicKey;
                    formattedPublicKey = strReplace(publicKey, "\\r\\n", '\r\n');
                    encryptedStream = pgpHelper.EncryptFile(streamName, formattedPublicKey);
                }
                
                if (keyVaultParameters.Encryption)
                {
                    System.IO.StreamReader streamReader = new System.IO.StreamReader(encryptedStream);
                	info (streamReader.ReadToEnd());
                }
                else
                {
                      System.IO.StreamReader streamReader = new System.IO.StreamReader(encryptedStream);
                      info (streamReader.ReadToEnd());
                }
    }
}

Helper class can also be designed using C# and .Net assembly can then be used to pass the file stream and key strings, but its just easier to manage that with x++ and is easy to debug in case of any issues. 

Below is equivalent C# Code for helper if  needed.
using System;
using System.Linq;
using System.Text;
using System.IO;
using Azure.Identity;
using Azure.Security.KeyVault.Secrets;
using Org.BouncyCastle.Bcpg.OpenPgp;
using Org.BouncyCastle.Security;

namespace FinTechDotNetLib
{
    public class PgpHelper
    {
        public Stream EncryptFile(Stream inputFile, string publicKeyString)
        {
            PgpPublicKey publicKey = ReadPublicKey(publicKeyString);

            MemoryStream outputStream = new MemoryStream();

            PgpEncryptedDataGenerator encryptedDataGenerator = new PgpEncryptedDataGenerator(Org.BouncyCastle.Bcpg.SymmetricKeyAlgorithmTag.Cast5, true, new SecureRandom());
            encryptedDataGenerator.AddMethod(publicKey);

            using (Org.BouncyCastle.Bcpg.ArmoredOutputStream armoredOut = new Org.BouncyCastle.Bcpg.ArmoredOutputStream(outputStream))
            {
                // Open the encrypted stream
                using (Stream encryptedOut = encryptedDataGenerator.Open(armoredOut, new byte[1 << 16]))
                {
                    //PgpCompressedDataGenerator compressedDataGenerator = new PgpCompressedDataGenerator(Org.BouncyCastle.Bcpg.CompressionAlgorithmTag.Uncompressed);
                    //Stream compressedOut = compressedDataGenerator.Open(encryptedOut);

                    // Write the input file data to the encrypted stream
                    byte[] buffer = new byte[1 << 16];
                    int len;
                    while ((len = inputFile.Read(buffer, 0, buffer.Length)) > 0)
                    {
                        encryptedOut.Write(buffer, 0, len);
                    }
                }
            }

            outputStream.Position = 0;

            return outputStream;
        }

        public Stream SignFile(Stream inputFile, string privateKeyString, string passphrase)
        {
            PgpPrivateKey privateKey = ReadPrivateKey(privateKeyString, passphrase);

            MemoryStream outputStream = new MemoryStream();

            PgpSignatureGenerator signatureGenerator = new PgpSignatureGenerator(Org.BouncyCastle.Bcpg.PublicKeyAlgorithmTag.RsaGeneral, Org.BouncyCastle.Bcpg.HashAlgorithmTag.Sha256);
            signatureGenerator.InitSign(PgpSignature.BinaryDocument, privateKey);

            PgpLiteralDataGenerator literalDataGenerator = new PgpLiteralDataGenerator();
            Stream literalOut = literalDataGenerator.Open(outputStream, PgpLiteralData.Binary, "filename", inputFile.Length, DateTime.UtcNow);

            byte[] buffer = new byte[1 << 16];
            int len;
            while ((len = inputFile.Read(buffer, 0, buffer.Length)) > 0)
            {
                literalOut.Write(buffer, 0, len);
                signatureGenerator.Update(buffer, 0, len);
            }

            //signatureGenerator.Generate().Encode(literalOut);
            PgpSignature signature = signatureGenerator.Generate();

            // Write the signature to the output stream
            using (Stream signatureStream = new MemoryStream())
            {
                signature.Encode(signatureStream);
                signatureStream.Position = 0;
                signatureStream.CopyTo(outputStream);
            }

            literalOut.Close();            

            outputStream.Position = 0;

            return outputStream;
        }

        private PgpPublicKey ReadPublicKey(string publicKeyString)
        {
            using (Stream keyIn = new MemoryStream(Encoding.UTF8.GetBytes(publicKeyString)))
            using (Stream inputStream = PgpUtilities.GetDecoderStream(keyIn))
            {
                PgpPublicKeyRingBundle publicKeyRingBundle = new PgpPublicKeyRingBundle(inputStream);
                foreach (PgpPublicKeyRing keyRing in publicKeyRingBundle.GetKeyRings())
                {
                    foreach (PgpPublicKey key in keyRing.GetPublicKeys())
                    {
                        if (key.IsEncryptionKey)
                        {
                            return key;
                        }
                    }
                }
            }
            throw new ArgumentException("Can't find encryption key in key ring.");
        }

        private PgpPrivateKey ReadPrivateKey(string privateKeyString, string passphrase)
        {
            using (Stream keyIn = new MemoryStream(Encoding.UTF8.GetBytes(privateKeyString)))
            using (Stream inputStream = PgpUtilities.GetDecoderStream(keyIn))
            {
                PgpSecretKeyRingBundle secretKeyRingBundle = new PgpSecretKeyRingBundle(inputStream);
                foreach (PgpSecretKeyRing keyRing in secretKeyRingBundle.GetKeyRings())
                {
                    foreach (PgpSecretKey key in keyRing.GetSecretKeys())
                    {
                        if (key.IsSigningKey)
                        {
                            PgpPrivateKey privateKey = key.ExtractPrivateKey(passphrase.ToCharArray());
                            if (privateKey != null)
                            {
                                return privateKey;
                            }
                        }
                    }
                }
            }
            throw new ArgumentException("Can't find signing key in key ring.");
        }
    }
}

Thanks for reading! Happy coding !!

Wednesday, 27 September 2023

Service update failure due to Globalupdate script for service model: AOSService on VM failed due to report deployment

Hello Folks, 

I am writing this post specifically to target the issue where step fails due to report deployment. For other issues, you can refer to

https://learn.microsoft.com/en-us/dynamics365/fin-ops-core/dev-itpro/deployment/deployable-package-troubleshooting

While applying recent service update 10.0.36 to my cloud VM, I have received an issue as 

GlobalUpdate script for service model: AOSService Failed on <VM Name>

To identify the exact issue, just navigate to 

C:\DynamicsAX\RunbookExecution-ExecuteParallelRunbook-<PackageId>\Output 

folder in your VM. 

Look for latest AxUpdateInstaller file, scroll through and you will find the error reason. 




Look for the failed step in the AxupdateInstaller file or mentioned failed step over LCS. In my case, it was step 25. Navigate to the folder with the same name. 

Since the issue was on report, look for latest UpdateReports or UpdateReportsOutput file. Scroll through to find the exact object the deployment has failed on 



As it says, In my case the issue was on CustProvisionalBalance.Report under ApplicationSuite model. 

To resolve this, we can just deploy this report through powershell command under VM. 

To deploy the report, run powershell as Admin and Navigate to   J:\AosService\PackagesLocalDirectory\Plugins\AxReportVmRoleStartupTask>

Then, execute the below command

.\DeployAllReportsToSsrs.ps1 -Module ApplicationSuite -PackageInstallLocation "J:\AosService\PackagesLocalDirectory" -ReportName CustProvisionalBalance.Report

You can change the report or Model name as needed. 

Thats it, Just resume the deployment from LCS. It should then go through with no issues.

Thanks!


Thursday, 19 January 2023

Perform operation at Entity level post all Target data Insert in D365 FnO

There are often requirements where we need to perform some operation(s) Just after target data is inserted/updated fully for all records. 

There is a method that can be used at Entity level known as postTargetProcess

Drawback of this method is that It only can be used with Data management framework in non-set based entities. 

It can also not be used with Odata operations. 

Method can be implemented as below

public class SaddafDemoEntity extends common
{   
    /// <summary>
    /// Post insert into target
    /// </summary>
    /// <param name = "_dmfDefinitionGroupExecution">DMFDefinitionGroupExecution</param>
    public static void postTargetProcess(DMFDefinitionGroupExecution _dmfDefinitionGroupExecution)
    {
       TestHeaderTable      headerTable;
       SaddafDemoStaging    staging;

        select firstonly forupdate integrationImportTable
            where integrationImportTable.DMFExecutionId == _dmfDefinitionGroupExecution.ExecutionId;

        select count(Recid) from staging
            where staging.DefinitionGroup   == _dmfDefinitionGroupExecution.DefinitionGroup
            && staging.ExecutionId          == _dmfDefinitionGroupExecution.ExecutionId
            && staging.TransferStatus       == DMFTransferStatus::Completed;

        ttsbegin;
        headerTable.RecordCount  = staging.RecId
        headerTable.update();
        ttscommit;
    }
}
For Entities using set based operations, we have a tricky workaround by using an Post event handler for a write method of DmfEntityWriter class. 

Point to be noted is, this Class and method is called for all the entities wo we need to make sure of using correct entity and logic inside that to avoid messing whole framework. 

Sample implemention is as below
   [PostHandlerFor(classStr(DmfEntityWriter), methodStr(DmfEntityWriter, write))]
   public static void DmfEntityWriter_Post_write(XppPrePostArgs args)
   {
       	DMFDefinitionGroupExecution  dmfDefinitionGroupExecution = args.getArg('_definitionGroupExecution');
       
       	str entityName = dmfDefinitionGroupExecution.EntityXMLName;

        switch (entityName)
        {
          case 'SaddafDemoEntity':
            TestHeaderTable      headerTable;
       		SaddafDemoStaging    staging;

        	select firstonly forupdate integrationImportTable
            	where integrationImportTable.DMFExecutionId == dmfDefinitionGroupExecution.ExecutionId;

        	select count(Recid) from staging
            	where staging.DefinitionGroup   == dmfDefinitionGroupExecution.DefinitionGroup
            	&& staging.ExecutionId          == dmfDefinitionGroupExecution.ExecutionId
            	&& staging.TransferStatus       == DMFTransferStatus::Completed;

        	ttsbegin;
        	headerTable.RecordCount  = staging.RecId
        	headerTable.update();
        	ttscommit;
          break;
        }
   }
Unfortunately there is nothing similar for Odata, but Odata has its own set of capabilities to supersede all shortcomings.
Thanks for reading!!

Perform operation at Entity level post all Staging data Insert or Before Target insert starts in D365 FnO

As the title suggests, there are sometimes requirements where we need to perform some operation(s) Just before target data insert operation triggers (or in between the whole DMF cycle) but not at the any datasource event or at the Entity insert method. 

One of such requirement can where the Any Header data to be populated before line insertion. 

Good news is we have a way to achieve it via postGetStagingData method at Entity level, Bad news is it works only after staging is inserted and works even if all target data fails validation.

Below is the implementation way for that 

public class SaddafDemoEntity extends common
{
    /// <summary>
    /// Post staging data
    /// </summary>
    /// <param name = "_dmfDefinitionGroupExecution">DMFDefinitionGroupExecution</param>
    public static void postGetStagingData(DMFDefinitionGroupExecution _dmfDefinitionGroupExecution)
    {
        TestHeaderTable      headerTable;
        SaddafDemoStaging    staging;

        select firstonly staging
            where staging.DefinitionGroup   == _dmfDefinitionGroupExecution.DefinitionGroup
            && staging.ExecutionId          == _dmfDefinitionGroupExecution.ExecutionId;

        headerTable.DMFExecutionId =  staging.ExecutionId; //Just the example, you can use any field needed
        headerTable.insert();
ttsbegin; staging = null; update_recordset staging setting ParentRefRecId = headerTable.RecId
where staging.DefinitionGroup == _dmfDefinitionGroupExecution.DefinitionGroup && staging.ExecutionId == _dmfDefinitionGroupExecution.ExecutionId; ttscommit; } }
For Entities using set based operations, we have another method known as preTargetProcessSetbased that can be implemented in similar way as above and is available to use just before Target process initiates (which is obvious by name😃). 

Happy Coding!!!

Wednesday, 16 September 2020

Multiselect lookup on dialog in UI builder class (Sysoperation) in D365 F&O

 This way of setting Multiselectlookup(Lookup that allows Multiple value selections) can be used on any module (SSRS, Service, Batch or simple server/client based service class) utilizing SysOperation framework. 

Code design would be as follows:- 

1. Data contract class: (just showing relevant parameters, there can be other parameters too). Just to note, container type parameter can also be used in place of List.

[DataContract, SysOperationContractProcessing(ClassStr(TestExpImpUIBuilder))]
class TESTEXPDC extends SysOperationServiceBaseDataContract
{
    List                department;    
    
    [DataMemberAttribute, SysOperationLabel("@SYS850"), AifCollectionTypeAttribute('return', Types::String)]
    public List parmDepartment(List _department = department)
    {
        department = _department;

        return department;
    }
}
2. UI Builder Class: This is most important to build a multi select lookup.
class TestExpImpUIBuilder extends SysOperationAutomaticUIBuilder
{
    DialogField         dialogDepartment;
    SysLookupMultiSelectCtrl    ctrlDepartment;
    
    //Override this to add dialog field
    protected DialogField addDialogField(IdentifierName _methodName, Object _dataContract = this.dataContractObject())
    {
        DialogField dialogField;;

        switch (_methodName)
        {
            case methodStr(TESTEXPDC, parmDepartment):
                dialogDepartment = this.dialog().addField(
                    extendedTypeStr(Description),
                    "@SYS850");
                dialogField = dialogDepartment;
                break;
            default:
            dialogField = super(_methodName, _dataContract);
        }
        return dialogField;
    }

    //Override this
    public void postRun()
    {
        super();
        if (this.dataContractObject() is TESTEXPDC) //if is optional, in my case i was using single UI builder for multiple tasks so required
        {
            this.lookupDepartment();
        }
    }
    
    //to populate lookup
    protected void lookupDepartment()
    {
        TableId                 multiSelectTableNum = tableNum(EcoResCategory);
        Query                   query               = new Query();
        QueryBuildDataSource    qbds                = query.addDataSource(multiSelectTableNum);

        EcoResCategoryHierarchyRole categoryRole;

        select firstonly categoryRole
            where categoryRole.NamedCategoryHierarchyRole == EcoResCategoryNamedHierarchyRole::RetailChannelNavigation;

        qbds.addRange(fieldNum(EcoResCategory, Level)).value(queryValue(3));
        qbds.addRange(fieldNum(EcoResCategory, CategoryHierarchy)).value(queryValue(categoryRole.CategoryHierarchy));
        qbds.addSelectionField(fieldNum(EcoResCategory, Code)); //needed for field display in lookup
        qbds.addSelectionField(fieldNum(EcoResCategory, Name));

        container selectedFields = [multiSelectTableNum, fieldNum(EcoResCategory, Code)]; //irrelevant for lookup but value needed for further use in method calling

        ctrlDepartment = SysLookupMultiSelectCtrl::constructWithQuery(this.dialog().dialogForm().formRun(), dialogDepartment.control(), query, false, selectedFields);
    }


    public void getFromDialog()
    {
        super();
        //keep in mind, multiselect actually brings Recid and works like reference lookup
        if (this.dataContractObject() is TESTEXPDC)
        {
            List        listDepartment  = new List(Types::String);
            container   conDeptt = ctrlDepartment.get();

            Counter conCount = 1;
            Counter conDepLength = conLen(conDeptt);

            while (conCount //use sign for less and equal here// conDepLength)
            {
                listDepartment.addEnd(EcoResCategory::find(conPeek(conDeptt, conCount)).Code);
                conCount++;
            }
            this.dataContractObject().parmDepartment(listDepartment);
            
            //whole list code not needed in case you are using container type parameter
            //this.dataContractObject().parmDepartment(ctrlDepartment.get());
        }
    }
}

3. Service class : Its optional and purely based upon requirement. No lookup code is needed here, just needed for execution type and Contract mapping to further call Task based class.

4. Task class: actual iteration of selected multiselect values would be done here.

class TESTEXPTask
{
    List                listDepartment;
    
    public static TESTEXPTask construct()
    {
        return new TESTEXPTask();
    }
    
    public void run(TESTEXPDC _contract)
    {
        this.processDataforExport();
    }
    
    protected void processDataforExport()
    {
        try
        {
            ttsbegin;
            // any code execution
          
            //Line Creation
            this.createLinesPerDepartment();
            ttscommit;

        }
        catch (Exception::Error)
        {
            exceptionTextFallThrough();
        }
    }
    
    protected void createLinesPerDepartment()
    {
        if (listDepartment && !listDepartment.empty())
        {
            ListEnumerator listEnumerator;

            listEnumerator = listDepartment.getEnumerator();
            while (listEnumerator.moveNext())
            {
                this.methodName(listEnumerator.current());
            }
        }
        else
        {
            //call execution code
            this.methodeName();
        }
    }

and you are done. Just squeeze in your other code like additional parameters and business code and voila!!!

Keep in mind, UI builder class in anyways would be used in similar old manner (overridelookup) for conventional field lookups.

See you in next blog. Happy Daxing (Now D365ing)!!! Please drop a feedback in case of any issues or any other concerns.