Tuesday, November 11, 2014

AX2012 Extension Framework

In a lot of ways, us X++ developers have gotten lazy over the years. If you take a step back, there are a lot of things we do today that you probably would never do in any other programming environment. Some of it is because it's just quick and easy, regardless of the less quick option being more robust or cleaner. In other cases, it's just because "that's how it's done" (tm) in the standard Microsoft Dynamics AX code.
Every now and then some new feature in the language or frameworks will come along, and it's either not documented or nobody really knows much about it. And in most cases it's barely used in standard code. Even things that everyone knows about, like events, are totally underused in the standard code base. To cut Microsoft some slack, they obviously have millions of lines of code that would have to be changed to adopt all these new features. And that obviously won't happen overnight.
Anyway, I wanted to point out this under-documented framework called the Extension Framework. I've only seen some minor blog posts about it, but I couldn't find it on MSDN or Technet anywhere. Regardless, I think this has great potential - especially for ISVs or Partner solutions that need to become more plug & play.

So, in this blog post we will create a totally unexciting set of classes. You'll recognize the standard AX pattern immediately. First, I have a base enum defining the different Dynamics products:


Next, I have a base class named Dynamics.


It has a new method that's made private (forcing people to use the static constructor) and a run method that has an exciting infolog message.

private void new()
{
}

public void Run()
{
    info("base class");
}

Next, we'll make a derived class for each of the Dynamics products:


Each one will extend the base class and override the Run method to show a different infolog. Below is the code for the DynamicsAX and DynamicsCRM class, you get the idea for the other classes...

public class DynamicsAX extends Dynamics
{
}

public void Run()
{
    super();

    info("Dynamics AX");
}

public class DynamicsCRM extends Dynamics
{
}

public void Run()
{
    super();

    info("Dynamics CRM");
}

Great. So now that we have our sub types, we can create the "classic" construct method on our base class. On the base class "Dynamics", create a new static construct method, which takes our base enum as a parameter, and returns the correct sub type for it.

public static Dynamics construct(DynamicsProduct _product)
{
    Dynamics dynamics = null;

    switch (_product)
    {
        case DynamicsProduct::AX:
            dynamics = new DynamicsAX();
            break;
        case DynamicsProduct::CRM:
            dynamics = new DynamicsCRM();
            break;
        case DynamicsProduct::GP:
            dynamics = new DynamicsGP();
            break;
        case DynamicsProduct::NAV:
            dynamics = new DynamicsNAV();
            break;
        case DynamicsProduct::SL:
            dynamics = new DynamicsSL();
            break;
        default:
            throw error("Unsupported product");
    }

    return dynamics;
}

Nothing too crazy here, a very traditional pattern you see a lot in Dynamics AX. Finally, we can create a static main method on the base class to run it. For the sake of this article, we'll just hard code the product to be AX (because that's what we're really here for, isn't?). Feel free to create menu items with parmenum or something, but to keep it short and to the point, hard coding will do. So again, on base class "Dynamics", add the main method:

public static void main(Args _args)
{
    Dynamics dynamics = Dynamics::construct(DynamicsProduct::AX);

    dynamics.Run();
}

And of course, when we run this, we'll see the infologs showing. The base class declaring it's the base, and the sub type declaring it's Dynamics AX. Perfect!

At this point, let's take a break and step back. What's wrong with this pattern? First of all, there is no way to extend this code base without either changing the code or overlayering (the construct method). Secondly, from a more design approach, it seems bad that the base class has to have explicit knowledge of all the sub classes. Put together, this is not very extension-friendly. If we would have two ISV products that both extend this, someone will have to merge the code. And nobody likes merging code.

So, extension framework! The idea is to use the new attributes feature in the X++ language to decorate the classes, and make a constructor that can dynamically find the right sub class, assuming there is one. So first, we need to create a new attribute. An attribute is basically a class that extends the base SysAttribute class. An attribute can have different properties, some mandatory, some optional. In this case, we want to use the attribute on a class to tie it back to one of the enum values. So, our attribute should have a property to hold that enum value. The base enum was called "DynamicsProduct" so we'll declare a member variable to hold that.
We'll call our attribute class "DynamicsProductAttribute".

class DynamicsProductAttribute extends SysAttribute
{
    DynamicsProduct product;
}

Since we don't really have class properties in X++, we need to create a set/get method, or as X++ likes to call it, a PARM method. Basically, a way to access the member variable to read or update it.

public DynamicsProduct parmDynamicsProduct(DynamicsProduct _product = product)
{
    product = _product;

    return product;
}

Now, to make this a mandatory property on the attribute we can add it as a method parameter on the new method. So, override the new method on the "DynamicsProductAttribute" class.

public void new(DynamicsProduct _dynamicsProduct)
{
    super();

    this.parmDynamicsProduct(_dynamicsProduct);
}

So, that's really it for the attribute class. Now we can actually start using it to decorate our sub classes. No need to touch the base class, but on all the sub classes, we want to add the attribute in the classdeclaration. Again, I'll just show the example for the DynamicsAX and Dynamics CRM classes:

[DynamicsProductAttribute(DynamicsProduct::AX)]
public class DynamicsAX extends Dynamics
{
}

[DynamicsProductAttribute(DynamicsProduct::CRM)]
public class DynamicsCRM extends Dynamics
{
}

Decorate each sub-class with the attribute and the correct enum value. This approach makes a whole lot more sense. Now each sub-class is declaratively tying back to a specific enum value. So finally, we of course need to change the constructor on the base class to make use of this at run time. This is really the only part where there's a "framework" really in place. On the base class "Dynamics", we're going to change the static construct method.
Now, we can ask the extension framework to grab us the class with a specific attribute. Of course we're interested in the DynamicsProductAttribute, but more importantly we're interested in the class that has that attribute but also has a specific value for its property (in our case, the enum DynamicsProduct). The extension framework just asks us what base class we want to grab sub classes for, as well as an INSTANCE of the attribute it should have. The reason we need an instance is because it needs all the properties (in our case we only have 1, but you could have several properties).
To get all this done, we can call the static "getClassFromSysAttribute" method on the "SysExtensionAppClassFactory" class. We give it the base class name ("Dynamics") and the attribute we want, which has to be an instance, so create an instance and set the correct property value. Your new and improved (and shortened) construct method should look like this now:

public static Dynamics construct(DynamicsProduct _product)
{
    Dynamics dynamics = SysExtensionAppClassFactory::getClassFromSysAttribute(classStr(Dynamics), new DynamicsProductAttribute(_product));

    return dynamics;
}

Since our static main was already using construct, you can just re-run the main and everything should work.
Note that this class factory business caches EVERYTHING. If you've run it once, and then re-run after making changes, it may not pick up new classes or changed attributes. From the Tools menu, you can run Tools / Caches / Refresh Elements to flush the cache so it will pick up your changes.

Let's review what we can do now.
- To extend this solution, we can add a new enum value. Create a new class, use the product attribute and tie it to the new enum value, and everything will work. No need to change any code, only adding code and decorating it!
- Of course, we use an enum here which isn't necessarily extension friendly, but there's nothing stopping you from using other data types, multiple properties, etc.
- The "choice" of which base class to use could be from a parameter per company, or based on a value in a record, whatever. The possibilities are bigger than you might think. Of course the traditional construct has this as well, but the ease of extending and swapping things out without having to mess with the code are pretty great.

7 comments:

  1. This is a really cool framework, but you are right it is a shame it not implemented very many places in standard. In standard you often see something like you first Construct, efficiently blocking us from extending by event handlers. Anyway, great article – thanks.

    ReplyDelete
  2. Great post! Thanks for the concise explanation.

    Also, I think you meant for the parm method to be a part of the DynamicsProductAttribute class, not DynamicsProduct. I was confused at first.

    ReplyDelete
    Replies
    1. Thanks!

      The text looks correct to me though. DynamicsProduct is the name of the enum, so that's the return value and parameter to the method... and it's followed right after the class declaration for the attribute. If i'm missing something please reply.

      Delete
    2. Ah, I see now. Thanks. I must have confused myself trying to keep the decorator, class, and enum straight.

      Delete
  3. Nice post, Joris! We actually use a third extension approach using a combination of reflecting over types and subtypes, and replacing the enum with entries in the database. There are pros and cons, but it helps avoid being dependent on that anoying enum. ;-)

    ReplyDelete
  4. I believe in RTM enum doesn't work when run in CIL. I don't have the exact error but you can get past it by using Tommy's suggestion. (simplest is convert an enum to a string value)

    ReplyDelete
  5. Thank you Joris! I found a bug in global cache http://alexvoy.blogspot.ca/2015/08/extension-framework-and-sysglobalcache.html

    ReplyDelete