web
You’re offline. This is a read only version of the page.
close
Skip to main content

Announcements

News and Announcements icon
Community site session details

Community site session details

Session Id :

How to use SysExtension framework effectively in Dynamics 365 FO

Bharani Preetham Peraka Profile Picture Bharani Preetham Pe... 3,634 Moderator

Building Scalable X++ Architecture Using the SysExtension Framework

 

The How?

The basic concept of Object-Oriented Programming (OOP) is simple: don't duplicate the same logic. That's why we have classes acting as templates where we can create multiple objects, and common methods that we can reuse in multiple places. In our Dynamics 365 FO world, even something as fundamental as an Extended Data Type (EDT) serves this exact purpose—reuse.

But lately, I found myself thinking. When I write complex logic for a specific purpose, and then a requirement comes in for something very similar with just minor changes, I often have to rewrite or copy-paste that same logic for the new scenario. I kept asking myself: Why am I doing this?

I felt that it would be so much better to have a common class where I could simply pass parameters and let it do the work. In simple layman's terms: just like a method works dynamically based on the parameter you pass it, shouldn't a class be able to do the same?

The Why?

Let's look at how we normally handle daily requirements with multiple variations.

Whether it is sending out different types of emails, generating different e-Invoices, or validating different types of records, developers generally end up writing a single, massive class packed with switch/case statements or if-else conditions.

If the type is Sales Order, do this...

Else if it's Transfer Order, do this...

This is essentially writing the same core logic over and over. Whenever the business wants a new variation, you have to open up this giant class, add a new case, and hope you don't accidentally break existing logic. It is risky, tightly coupled, and honestly, it just doesn't look good for reading.

Instead of that clumsy approach, isn't there something better, cooler, and more straightforward? I started digging deeper into the standard base code, and finally, I found the perfect solution.

The Solution

We can solve this beautifully by combining two powerful concepts: the Template Method Design Pattern and the SysExtension Framework.

With this approach, there are absolutely zero switch cases and no extra if-else conditions.

The Approach

Think of the SysExtension framework as a "Matchmaker."

Normally, a switch statement acts as the director, explicitly telling the code which class to use. SysExtension completely eliminates this. It works on reflection, meaning it reads the metadata of your code at runtime to automatically find the correct class for the job.

The secret sauce is this single standard line of code:

SysExtensionAppClassFactory::getClassFromSysAttribute(
           classStr(SendEmailBase),
           attribute) as SendEmailBase;
 

Here is what this line does:

During the build process, the system scans your code and builds a lightning-fast reflection cache of all your 'Name Tags' (Attributes). At runtime, when your code runs, it simply pings that cache, instantly finds the child class wearing the matching tag, and creates an instance of it without a single if statement.

Everything is dynamic. Going forward, if we need to add 50 new variations, we just create the child classes, slap the right attribute tag on them, and we never have to touch the base class again.

Where Does Standard D365FO Use This?

If you are wondering if this is a standard practice, it absolutely is! Microsoft uses the SysExtension framework heavily across the core product. Here are a few places you interact with it daily:

  • SalesFormLetter: The base replaced the switch case pattern with attribute decoration like [DocumentStatusFactoryAttribute(DocStatus::MiscInvoice)] on each sub class.

  • SalesLineCopyFromSource: This class is used to copy the data from various source for example quotation, other saleslines, templates etc. SalesLineCopyFromSourceFactoryAttribute is used for instantiating the SalesLineCopyFromSource class.

Day-to-Day Use Cases: Where Else Can We Leverage This?

This pattern is not just for emails. It is the ultimate solution for any daily requirement where the "wrapper" (the standard steps) is the same, but the "filling" (the specific data or logic) changes. Here are three common, real-world D365FO use cases:

Use Case 1: Custom CSV/Excel File Exports

Requirement: The business needs a nightly batch job that exports data into separate CSV files. The way we create a file is the same, but the columns and tables for Customers, Vendors, and Items are completely different.

The Attribute: An enum for ExportFileType (e.g., Customer, Vendor, Item).

The Base Class: Defines the master rules: createFile(), writeHeader(), writeLines(), and closeAndSendFile().

The Concrete Class: ExportCustomerCsv. It only fills in the blanks for the abstract methods—it defines that the header is "AccountNum, Name" and loops through CustTable to write the lines.

Use Case 2: E-Invoice/E-Way Integrations

Requirement: Generating government e-Invoices. We have to send a JSON payload to a portal, but the data structure changes completely depending on whether it is a Sales Order Invoice, a Free Text Invoice, or a Transfer Order.

The Attribute: An enum for EInvDocumentType (SalesOrder, FreeText, TransferOrder).

The Base Class: Dictates the standard API rules: fetchAuthToken(), buildJsonPayload(), sendHttpRequest(), and logResponse().

The Concrete Class: EInvTransferOrder. This class is solely responsible for writing the specific X++ code that formats a Transfer Order into the required JSON structure. The base class handles actually sending it over the internet.

Use Case 3: Pre-Posting Data Validation Rules

Requirement: The business wants strict, custom validation rules before users can submit records to a custom workflow. A Sales Order needs a credit limit check, a Purchase Order needs a budget check, and a Vendor Invoice needs a matching check.

The Attribute: An enum for ValidationDocType.

The Base Class: Controls the execution flow: fetchRecord(), runValidation(), and logErrorsToInfolog().

The Concrete Class: ValidateSalesOrder. It only contains the specific logic required for runValidation() to check the credit limits.

Step-by-Step Code Example: Building an Extensible Engine

To see this in action, let's look at how we can use this exact pattern to build a highly scalable automated email engine.

1. The Metadata Tag (The Attribute) First, we must create a custom Attribute class. Think of this simply as a custom sticky note or "Name Tag." We will link our base enum (EmailTemplateType) to this tag.

class EmailTemplateTypeAttribute extends SysAttribute
{
   EmailTemplateType docType;

   public void new(EmailTemplateType _docType)
   {
       docType = _docType;
   }

   public EmailTemplateType parmDocType()
   {
       return docType;
   }
}
 

2. The Master Blueprint (Abstract Base Class) Here, we couple the Template Method pattern with SysExtension. The processAndSend() method is the rulebook. It defines the unchangeable sequence of events (Set To, Set CC, Set Subject, Attach Report, Send).

Notice that the actual heavy-lifting methods at the bottom are abstract. The base class is saying, "I know the steps to send an email, but I'll let my child classes figure out the specific details." The construct() method acts as our matchmaker.

abstract class SendEmailBase
{
   protected SysMailerMessageBuilder messageBuilder;
   protected common callerRecord;
   protected EmailTemplate emailtemplateTable;

   protected void new()
   {
       messageBuilder = new SysMailerMessageBuilder();
   }

   public void parmCallerRecord(common _callerRecord)
   {
       callerRecord = _callerRecord;
   }

   public void parmTemplateTable(EmailTemplate _emailtemplateTable)
   {
       emailtemplateTable = _emailtemplateTable;
   }

   /// <summary>
   /// Factory method using SysExtension
   /// </summary>
   public static SendEmailBase construct(EmailTemplateType _docType)
   {
       EmailTemplateTypeAttribute attribute = new EmailTemplateTypeAttribute(_docType);
       
       SendEmailBase sender = SysExtensionAppClassFactory::getClassFromSysAttribute(
           classStr(SendEmailBase),
           attribute) as SendEmailBase;

       if (!sender)
       {
           throw error(strFmt("No email handler implemented for document type %1", _docType));
       }

       return sender;
   }

   /// <summary>
   /// Master execution workflow. The caller ONLY runs this.
   /// </summary>
   public void processAndSend()
   {
       // 1. Set To Address
       List toAddressList = this.determineToAddress();
       if (toAddressList.empty())
       {
           throw Error(strFmt("To address is empty. Cannot send email"));
       }
       
       ListEnumerator toAddressListEnum = toAddressList.getEnumerator();
       while (toAddressListEnum.moveNext())
       {
           messageBuilder.addTo(toAddressListEnum.current());
       }

       // 2. Set CC Address (Optional)
       List ccAddressList = this.determineCcAddress();
       ListEnumerator ccAddressListEnum = ccAddressList.getEnumerator();
       while (ccAddressListEnum.moveNext())
       {
           messageBuilder.addCc(ccAddressListEnum.current());
       }

       // 3. Set Subject and Body
       messageBuilder.setSubject(this.determineSubject());
       messageBuilder.setBody(this.determineBody());

       SMTPUserName fromAddr = SysEmailParameters::find().SMTPUserName;
       if (!fromAddr) throw Error("From Address is empty. Cannot send email");
       messageBuilder.setFrom(fromAddr);

       // 4. Generate and attach the SSRS report (or any file)
       this.buildAndAttachReport();

       // 5. Send via standard framework
       SysMailerFactory::sendNonInteractive(messageBuilder.getMessage());
   }

   // The blanks that specific classes need to fill in
   protected abstract List determineToAddress() {}
   protected abstract List determineCcAddress() {}
   protected abstract str determineSubject() {}
   protected abstract str determineBody() {}
   protected abstract void buildAndAttachReport() {}
}
 

3. The Trigger Class (The Middleman) This class simply acts as a clean entry point to kick off the base class process.

class SendEmailProcess
{
   public static void runEmailTrigger(Common _common, EmailTemplateType _templateType)
   {
       EmailTemplate templateTable;
       
       SendEmailBase emailSender = SendEmailBase::construct(_templateType);
       emailSender.parmCallerRecord(_common);

       select firstonly templateTable
           where templateTable.TemplateType == _templateType;

       emailSender.parmTemplateTable(templateTable);
       emailSender.processAndSend();
   }
}
 

4. The Concrete Implementation (The Child Class) Now that we have our template, we need a specific class to handle Sales Invoices. Notice the [EmailTemplateTypeAttribute(EmailTemplateType::SalesInvoice)] at the very top. That is the name tag! Because this class extends our base class, it is forced to provide the actual logic for those blank abstract methods.

[EmailTemplateTypeAttribute(EmailTemplateType::SalesInvoice)]
class SendEmailSalesInvoice extends SendEmailBase
{
   protected List determineToAddress()
   {
       CustInvoiceJour custInvoiceJour = callerRecord as CustInvoiceJour;
       List toAddressList = new List(Types::String);
       Email toEmail = custInvoiceJour.custTable_InvoiceAccount().email();

       if (toEmail) 
           toAddressList.addEnd(custInvoiceJour.custTable_InvoiceAccount().email());
       
       return toAddressList;
   }

   protected List determineCcAddress()
   {
       List ccAddressList = new List(Types::String);
       ccAddressList = con2List(str2con(emailtemplateTable.CC, ','));
       return ccAddressList;
   }

   protected str determineSubject()
   {
       return emailtemplateTable.EmailSubject;
   }

   protected str determineBody()
   {
       return emailtemplateTable.EmailBody;
   }

   protected void buildAndAttachReport()
   {
       //Assuming I have proper report file buffer in streamIo and fileName has report name and not writing code for this        
       messageBuilder.addAttachment(streamIo, fileName);
   }
}
 

5. The Final Call (Chain of Command) When we actually need to trigger this whole engine (for example, right after an invoice posts), our calling code is beautifully reduced to just a single, readable line.

[ExtensionOf(classStr(SalesFormLetter_Invoice))]
final class SalesFormLetter_Invoice_Extension
{
   public void run()
   {
       next run();

       SalesParmUpdate salesParmUpdate = this.salesParmUpdate();

       if (salesParmUpdate && salesParmUpdate.ParmId)
       {
           CustInvoiceJour invoiceJour;

           while select invoiceJour
               where invoiceJour.ParmId == salesParmUpdate.ParmId
           {
               SendEmailProcess::runEmailTrigger(invoiceJour, EmailTemplateType::SalesInvoice);
           }
       }
   }
}
 

The Business Value

Why go through the effort of setting this up?

Infinitely Scalable: Following the Open/Closed principle, this architecture is open for extension but closed for modification. You can add as many new file exports, eInv payload structures, or email types as the business requests without ever modifying the original base class.

True Code Reusability: The standard framework code for SMTP connections, writing CSV headers, or authenticating APIs is written exactly once in the base class. It is never duplicated.

Ultra-Clean Extensions: The calling code is reduced to a single line. It is easy to read, easy to debug, and incredibly stable.

Conclusion

Moving away from massive switch/case statements might take a little bit of extra thought upfront when designing your base class, but the long-term payoff is massive.

Your future self (and any fellow developer who has to read your code later) will thank you:)

Happy coding!

 

This is originally posted here.

 

Comments