Code highlighting

Showing posts with label SysExtension. Show all posts
Showing posts with label SysExtension. Show all posts

Friday, March 31, 2017

Development tutorial: SysExtension framework with SysExtensionIAttribute and an Instantiation strategy

Problem statement

This post will continue from where we left off in my previous blog post about the SysExtension framework: Development tutorial: SysExtension framework in factory methods where the constructor requires one or more arguments

In the above blog post I described how to use the SysExtension framework in combination with an Instantiation strategy, which applies in many cases where the class being instantiated requires input arguments in the constructor.

At the end of the post, however, I mentioned there is one flaw with that implementation. That problem is performance.

If you remember a blog post by mfp from a while back (https://blogs.msdn.microsoft.com/mfp/2014/05/08/x-pedal-to-the-metal/), in it he describes the problems with the SysExtension framework in AX 2012 R2, where there were two main issues:
  • A heavy use of reflection to build the cacheKey used to look up the types
  • Interop impact when needing to make Native AOS calls instead of pure IL
The second problem is not really relevant in Dynamics 365 for Operations, as everything runs in IL now by default.

And the first problem was resolved through introduction of an interface, SysExtensionIAttribute, which would ensure the cache is built by the attribute itself and does not require reflection calls, which immediately improved the performance by more than 10x.

Well, if you were paying attention to the example in my previous blog post, you noticed that my attribute did not implement the above-mentioned interface. That is because using an instantiation strategy in combination with the SysExtensionIAttribute attribute was not supported.

It becomes obvious if you look at the comments in the below code snippet of the SysExtension framework:
public class SysExtensionAppClassFactory extends SysExtensionElementFactory
{
    ...
    public static Object getClassFromSysAttribute(
        ClassName       _baseClassName,
        SysAttribute    _sysAttribute,
        SysExtAppClassDefaultInstantiation  _defaultInstantiation = null
        )
    {
        SysExtensionISearchStrategy         searchStrategy;
        SysExtensionCacheValue              cachedResult;
        SysExtModelAttributeInstance        attributeInstance;
        Object                              classInstance;
        SysExtensionIAttribute              sysExtensionIAttribute = _sysAttribute as SysExtensionIAttribute;

        // The attribute implements SysExtensionIAttribute, and no instantiation strategy is specified
        // Use the much faster implementation in getClassFromSysExtAttribute().
        if (sysExtensionIAttribute && !_defaultInstantiation)
        {
            return SysExtensionAppClassFactory::getClassFromSysExtAttribute(_baseClassName, sysExtensionIAttribute);
        }
        ...
    }
    ...
}

So if we were to use an Instantiation strategy we would fall back to the "old" way that goes through reflection. Moreover, it would actually not work even then, as it would confuse the two ways of getting the cache key.

That left you with one of two options:

  • Not implement the SysExtensionIAttribute on the attribute and rip the benefits of using an instantiation strategy, but suffer the significant performance hit it brings with it, or
  • Use the SysExtensionIAttribute, but as a result not be able to use the instantiation strategy, which limited the places where it was applicable

No more! 

We have updated the SysExtension framework in Platform Update 5, so now you can rip the benefits of both worlds, using an instantiation strategy and implementing the SysExtensionIAttribute interface on the attribute.

Let us walk through the changes required to our project for that:

1.

First off, let's implement the interface on the attribute definition. We can now also get rid of the parm* method, which was only necessary when the "old" approach with reflection was used, as that was how the framework would retrieve the attribute value to build up the cache key. 

class NoYesUnchangedFactoryAttribute extends SysAttribute implements SysExtensionIAttribute
{
    NoYesUnchanged noYesUnchangedValue;

    public void new(NoYesUnchanged _noYesUnchangedValue)
    {
        noYesUnchangedValue = _noYesUnchangedValue;
    }

    public str parmCacheKey()
    {
        return classStr(NoYesUnchangedFactoryAttribute)+';'+int2str(enum2int(noYesUnchangedValue));
    }

    public boolean useSingleton()
    {
        return true;
    }

}

As part of implementing the interface we needed to provide the implementation of a parmCacheKey() method, which returns the cache key taking into account the attribute value. We also need to implement the useSingleton() method, which determines if the same instance should be returned by the extension framework for a given extension.

The framework will now rely on the parmCacheKey() method instead of needing to browse through the parm methods on the attribute class.

2.

Let's now also change the Instantiation strategy class we created, and implement the SysExtensionIInstantiationStrategy interface instead of extending from SysExtAppClassDefaultInstantiation. That is not necessary now and is cleaner this way.

public class InstantiationStrategyForClassWithArg implements SysExtensionIInstantiationStrategy
{
...
}

The implementation should stay the same.

3. 

Finally, let's change the construct() method on the base class to use the new API, by calling the getClassFromSysAttributeWithInstantiationStrategy() method instead of getClassFromSysAttribute() (which is still there for backward compatibility):

public class BaseClassWithArgInConstructor
{
...
    public static BaseClassWithArgInConstructor construct(NoYesUnchanged _factoryType, str _argument)
    {
        NoYesUnchangedFactoryAttribute attr = new NoYesUnchangedFactoryAttribute(_factoryType);
        BaseClassWithArgInConstructor inst = SysExtensionAppClassFactory::getClassFromSysAttributeWithInstantiationStrategy(
            classStr(BaseClassWithArgInConstructor), attr, InstantiationStrategyForClassWithArg::construct(_argument));
        
        return inst;
    }
}

Result

Running the test now will produce the following result in infolog:

The derived class was returned with the argument populated in

Download

You can download the full project for the updated example from my OneDrive.


Hope this helps!

Saturday, March 18, 2017

Development tutorial: SysExtension framework in factory methods where the constructor requires one or more arguments

Background

At the Dynamics 365 for Operations Technical Conference earlier this week, Microsoft announced its plans around overlayering going forward. If you have not heard it yet, here's the tweet I posted on it:
The direct impact of this change is that we should stop using certain patterns when writing new X++ code.

Pattern to avoid

One of these patterns is the implementation of factory methods through a switch block, where depending on an enumeration value (another typical example is table ID) the corresponding sub-class is returned.

First off, it's coupling the base class too tightly with the sub-classes, which it should not be aware of at all.
Secondly, because the application model where the base class is declared might be sealed (e.g, foundation models are already sealed), you would not be able to add additional cases to the switch block, basically locking the application up for any extension scenarios.

So, that's all good and fine, but what can and should we do instead?

Pattern to use

The SysExtension framework is one of the ways of solving this erroneous factory method pattern implementation. 

This has been described in a number of posts already, so I won't repeat here. Please read the below posts instead, if you are unfamiliar with how SysExtension can be used:
In many cases in existing X++ code, the constructor of the class (the new() method) takes one or more arguments. In this case you cannot simply use the SysExtension framework methods described above. 

Here's an artificial example I created to demonstrate this:
  • A base class that takes one string argument in the constructor. This could also be abstract in many cases.
  • Two derived classes that need to be instantiated depending on the value of a NoYesUnchanged enum passed into the construct() method.
public class BaseClassWithArgInConstructor
{
    public str argument;

    
    public void new(str _argument)
    {
        argument = _argument;
    }

    public static BaseClassWithArgInConstructor construct(NoYesUnchanged _factoryType, str _argument)
    {
        // Typical implementation of a construct method
        switch (_factoryType)
        {
            case NoYesUnchanged::No:
                return new DerivedClassWithArgInConstructor_No(_argument);
            case NoYesUnchanged::Yes:
                return new DerivedClassWithArgInConstructor_Yes(_argument);
        }

        return new BaseClassWithArgInConstructor(_argument);
    }
}

public class DerivedClassWithArgInConstructor_No extends BaseClassWithArgInConstructor
{
}

public class DerivedClassWithArgInConstructor_Yes extends BaseClassWithArgInConstructor
{
}

And here's a Runnable class we will use to test our factory method:

class TestInstantiateClassWithArgInConstructor
{        
    public static void main(Args _args)
    {        
        BaseClassWithArgInConstructor instance = BaseClassWithArgInConstructor::construct(NoYesUnchanged::Yes, "someValue");
        setPrefix("Basic implementation with switch block");
        info(classId2Name(classIdGet(instance)));
        info(instance.argument);
    }
}

Running this now would produce the following result:
The right derived class with the correct argument value returned
OK, so to decouple the classes declared above, I created a "factory" attribute, which takes a NoYesUnchanged enum value as input.

public class NoYesUnchangedFactoryAttribute extends SysAttribute
{
    NoYesUnchanged noYesUnchangedValue;

    public void new(NoYesUnchanged _noYesUnchangedValue)
    {
        noYesUnchangedValue = _noYesUnchangedValue;
    }

    public NoYesUnchanged parmNoYesUnchangedValue()
    {
        return noYesUnchangedValue;
    }
}

Let's now decorate the two derived classes and modify the construct() on the base class to be based on the SysExtension framework instead of the switch block:

[NoYesUnchangedFactoryAttribute(NoYesUnchanged::No)]
public class DerivedClassWithArgInConstructor_No extends BaseClassWithArgInConstructor
{
}

[NoYesUnchangedFactoryAttribute(NoYesUnchanged::Yes)]
public class DerivedClassWithArgInConstructor_Yes extends BaseClassWithArgInConstructor
{
}

public class BaseClassWithArgInConstructor
{
    // ...
    public static BaseClassWithArgInConstructor construct(NoYesUnchanged _factoryType, str _argument)
    {
        NoYesUnchangedFactoryAttribute attr = new NoYesUnchangedFactoryAttribute(_factoryType);
        BaseClassWithArgInConstructor instance = SysExtensionAppClassFactory::getClassFromSysAttribute(classStr(BaseClassWithArgInConstructor), attr);

        return instance;
    }
}

Running the test now will however not produce the expected result:
The right derived class is returned but argument is missing
That is because by default the SysExtension framework will instantiate a new instance of the corresponding class (dictClass.makeObject()), which ignores the constructor arguments.

Solution

In order to account for the constructor arguments we need to use an Instantiation strategy, which can then be passed in as the 3rd argument when calling SysExtensionAppClassFactory.

Let's define that strategy class:

public class InstantiationStrategyForClassWithArg extends SysExtAppClassDefaultInstantiation
{
    str arg;

    public anytype instantiate(SysExtModelElement  _element)
    {
        SysExtModelElementApp   appElement = _element as SysExtModelElementApp;
        Object                  instance;

        if (appElement)
        {
            SysDictClass dictClass = SysDictClass::newName(appElement.parmAppName());
            if (dictClass)
            {
                instance = dictClass.makeObject(arg);
            }
        }

        return instance;
    }

    protected void new(str _arg)
    {
        this.arg = _arg;
    }

    public static InstantiationStrategyForClassWithArg construct(str _arg)
    {
        return new InstantiationStrategyForClassWithArg(_arg);
    }
}

As you can see above, we had to

  • Define a class extending from SysExtAppClassDefaultInstantiation (it's unfortunate that it's not an interface instead). 
  • Declare all of the arguments needed by the corresponding class we plan to construct.
  • Override the instantiate() method, which is being invoked by the SysExtension framework when the times comes
    • In there we create the new object instance of the appElement and, if necessary, pass in any additional arguments, in our case, arg.
Let's now use that in our construct() method:

public class BaseClassWithArgInConstructor
{
    //...
    public static BaseClassWithArgInConstructor construct(NoYesUnchanged _factoryType, str _argument)
    {
        NoYesUnchangedFactoryAttribute attr = new NoYesUnchangedFactoryAttribute(_factoryType);
        BaseClassWithArgInConstructor instance = SysExtensionAppClassFactory::getClassFromSysAttribute(
            classStr(BaseClassWithArgInConstructor), attr, InstantiationStrategyForClassWithArg::construct(_argument));

        return instance;
    }
}

If we now run the test, we will see the following:
The right derived class with the correct argument value returned 

Note

If you modify the attributes/hierarchy after the initial implementation, you might need to clear the cache, and restarting IIS is not enough, since the cache is also persisted to the DB. You can do that by invoking the below static method:

SysExtensionCache::clearAllScopes();

Parting note

There is a problem with the solution described above. The problem is performance.
I will walk you through it, as well as the solution, in my next post.