Thumbnail image

Embrace the Extensions Mindset with Dynamics 365 for Finance and Operations #2 – SysExtension Framework [ENG]

!
Warning: This post is over 365 days old. The information may be out of date.

Table of Contents

In my previous post Embrace the extensions mindset with Dynamics 365 for Finance and Operations we reflected on some of the patterns we can leverage to create our customizations by using only non-intrusive changes based on a real example: Adding a new Number Sequence to a standard module.

In particular, we discussed:

  • Metadata Extensions — to add a new_ Enum Value in a standard Base Enum.
  • Class Extensions — to add a new method to a standard class.
  • Chain-of-Command — to add a block of code to an existing standard class method.
  • Event-Handler subscription — to subscribe our own method to an existing standard delegate provided as an extension point.

If you still didn’t read that blog post, please take a moment now. If you already did it (thanks!), let’s continue with another pattern we have to consider: SysExtension Framework (also SysPlugin, that is quite similar).

This pattern allows us to create new sub-classes for factory methods without any over-layering or coupling with the original hierarchy. Therefore, we can add new sub-classes in our own packages without any intrusive modifications and replace a very common pattern, widely used all over the application, like this (taken from AX 2012 R3):

static SalesCopying construct(SalesPurchCopy salesPurchCopy)
{
    switch(salesPurchCopy)
    {
        case SalesPurchCopy::CreditNoteHeader       : return new SalesCopying_CreditNote();
        case SalesPurchCopy::CreditNoteLines        : return SalesCopyingCreditNoteLine::construct();

        case SalesPurchCopy::CopyAllHeader          :
        case SalesPurchCopy::CopyAllLines           :
        case SalesPurchCopy::CopyJournalHeader      :
        case SalesPurchCopy::CopyJournalLines       : return new SalesCopying();

        // 
        case SalesPurchCopy::VoidFiscalDocument_BR  : return new SalesCopying_VoidFiscalDocument_BR();
        // 

        default                                     : throw error(strFmt("@SYS19306",funcName()));
    }

    throw error(strFmt("@SYS19306",funcName()));
}

This pattern has many of problems to be extensible. The most obvious is likely the throw error on the default case, that makes impossible to an extension class to subscribe to the Post event on the method to add new cases. But even deleting this throw sentence (that has been indeed deleted in many standard methods as a quick way to make them extensible), the pattern itself is still a problem. If a new customer or partner customization or even a new standard module needs a new case, this class needs to be modified and the full package compiled and deployed.

Nevertheless, having such Factory pattern is useful, so the SysExtension (and SysPlugin) frameworks allow us to create the same behavior with a single constructor but without all the coupling created by the switch-case statement. The SysExtension framework uses an attribute class to identify the hierarchy decide its own particular behavior, and this attribute is used by a generic constructor (the Factory) to instantiate the right sub-class depending on the constructor parameters. The SysExtensionIAttribute guide us on how to implement new sub-classes. Such attribute classes will have these relations:

As usual, the best way to learn is to look at the standard code. We have a good example of this pattern in the SalesLineCopyFromSource hierarchy (SalesLineCopy class and subclasses in Dynamics AX 2012). This is the attribute class extracted from MSDyn365FO 8.0:

/// <summary>
/// The SalesLineCopyFromSourceFactoryAttribute is an attribute used for instantiating the SalesLineCopyFromSource class.
/// </summary>
class SalesLineCopyFromSourceFactoryAttribute extends SysAttribute implements SysExtensionIAttribute
{
    TableName tableName;

    public void new(TableName _tableName)
    {
        tableName = _tableName;
    }

    public str parmCacheKey()
    {
        return classStr(SalesLineCopyFromSourceFactoryAttribute)+';'+tableName;
    }

    public boolean useSingleton()
    {
        return false;
    }
}

And here is the Factory method, the static constructor that will return a sub-class instance depending on the received parameters. See the 2 first lines as this is mainly where the instance is created based on the attribute class, that will be the same on all classes for a specific factory hierarchy:

abstract class SalesLineCopyFromSource
{
    public static SalesLineCopyFromSource construct(SalesLine _salesLine, SalesTable _salesTable, TmpFrmVirtual _tmpFrmVirtualLines, SalesCopying _salesCopying)
    {
        SalesLineCopyFromSourceFactoryAttribute attr = new SalesLineCopyFromSourceFactoryAttribute(tableId2Name(_tmpFrmVirtualLines.TableNum));
        SalesLineCopyFromSource salesLineCopyFromSource = SysExtensionAppClassFactory::getClassFromSysAttribute(classStr(SalesLineCopyFromSource), attr) as SalesLineCopyFromSource;

        if (!salesLineCopyFromSource)
        {
            throw error(Error::wrongUseOfFunction(funcName()));
        }

        salesLineCopyFromSource.initialize(_salesLine, _salesTable, _tmpFrmVirtualLines, _salesCopying);
        return salesLineCopyFromSource;
    }
}

This simple constructor will automatically detect new sub-classes when decorated with the specified attribute without any modification, including throwing an error if a subclass is not found, something that should never happen anyway. All the sub-classes look like that:

/// <summary>
/// The SalesLineCopyFromSource class is responsible for copying SalesLine from a SalesLine.
/// </summary>
[SalesLineCopyFromSourceFactory(tableStr(SalesLine))]
class SalesLineCopyFromSalesLine extends SalesLineCopyFromSource
{
    ...
}

/// <summary>
/// The SalesLineCopyFromSource class is responsible for copying SalesLine from a CustConfirmTrans.
/// </summary>
[SalesLineCopyFromSourceFactory(tableStr(CustConfirmTrans))]
class SalesLineCopyFromCustConfirmTrans extends SalesLineCopyFromSource
{
    ...
}

We can see an example on how to consume this hierarchy, for example in the class SalesCopying, copyLines method. On this case, this class is used the same way that on the previous version, there is no change on the consumer side (except the new class name):

SalesLineCopyFromSource salesLineCopyFromSource = **SalesLineCopyFromSource::construct(salesLine, salesTable, tmpFrmVirtualLines, this);**

// ... deleted for simplicity

salesLineCopyFromSource.copy();

Full hierarchy looks like that:

Now, imagine we want to add a new sub-class to this structure or a similar one. We only need to create our very own class and decorate it with the hierarchy attribute. For this example, let’s say we want to extend this structure to work with the MyTable table:

[SalesLineCopyFromSourceFactory(tableStr(MyTable))]
class SalesLineCopyFromMyTable extends SalesLineCopyFromSource
{
    ....
}

Then, as we are inheriting the SalesLineCopyFromSource abstract class the compiler will complain if we don’t implement all the abstract methods from the super-class:

Looking to one of the sub-classes is easy to understand how to implement these methods (I have not tested myself if this implementation works, just showing it for illustrative purposes):

[SalesLineCopyFromSourceFactory(tableStr(MyTable))]
class SalesLineCopyFromMyTable extends SalesLineCopyFromSource
{
    MyTable myTable;

    public Qty retrieveSourceQty()
    {
        return myTable.SalesQty;
    }

    public Common retrieveSource()
    {
        return myTable;
    }

    public InventTransId retrieveSourceInventTransId()
    {
        return myTable.InventTransId;
    }
}

You may have already noticed that there are no extensions at all on this pattern. Just plain and pure Clean Code and object-oriented design that will be extensible by its own merits. Therefore, in our upcoming journey to change our mindset to extension-based customization, it is particularly important that we start thinking about clean code as a way to develop extensible code from the beginning and be prepared to identify such patterns. Refactors like the shown here will often happen to the standard code as a result of extensibility requests received from partners and customers.

NOTE: surprisingly enough this framework is not exactly new, it was available in Dynamics AX 2012 but its use was never fully adopted. Its time has come now with our mindset switch!

Published first at “Dynamics AX in the Field”, the blog from the Premier Field Engineering team at Microsoft.

Posts in this series