Pack and Unpack

Packing and unpacking is a bit of an older concept in Dynamics AX / Dynamics 365, but sometimes code like this is encountered, and if you aren’t familiar with containers, macros, and what the code is there for in the first place, it can be rather confusing.

Pack and unpack is Dynamics AX language for serialization — putting object-specific data in a format that fits into a database field and later reading it back to create an object in (more or less) the exact same state.

Most of the interactive objects in Dynamics AX will automatically check for saved user data when invoked and instantiate accordingly.  Perhaps you may remember running a report and the next time you opened the report the query and parameter values were last values you used.  This happens because of pack and unpack.

The majority of what makes pack and unpack confusing is macros, so let’s do a simple example not using macros.  Let’s say we have a class that has two date values which needs to persist between runs: startDate and endDate.

When pack() gets called, both variables are stored.  (If you don’t understand containers, look them up on MSDN first, otherwise this won’t make sense).

container pack()
{
    return [startDate, endDate];
}

The framework (for example, RunBase) will take whatever pack() returns and store it in the user data table.  So when the object is instantiated, unpack() gets called in order to reinitialize the values.

boolean unpack(container _packedValues)
{
    [startDate, endDate] = _packedValues;
    return true;
}

Follow what’s happening here?  The _packedValues parameter is expected to contain startDate and endDate in that order, so we assign those values to the class variables (inside a makeshift container).

Now let’s imagine we want to add another class variable that needs to be stored: isPosted

To do this, pack() could be modified as follows:

container pack()
{
    return [isPosted, startDate, endDate];
}

and unpack() to assign all three:

boolean unpack(container _packedValues)
{
    [isPosted, startDate, endDate] = _packedValues;
    return true;
}

Hm, but what about values that were stored before our modification?  That container assignment won’t work correctly (if this was executed at least once before) because _packedValues will only contain 2 values (startDate and endDate) and the previous value for startDate (a date) will be assigned to isPosted (a boolean).  Oops.

So let’s rewind the tape back to the first implementation. The first value in packed data now identifies the “version” of the packed data so when unpacking, it can be determined if the data can be unpacked without incorrect variable assignment or crashing.

container pack()
{
    return [1, startDate, endDate];
}
boolean unpack(container _packedValues)
{
    boolean ret = true;
    int version = conpeek(_packedValues, 1); 

    if (version == 1)
    {
        [version, startDate, endDate] = _packedValues;
    }
    else
    {
        ret = false;
    }
    return ret;
}

When implemented this way, if more variables are required, the version number gets incremented in both pack() and unpack(), but now attempting to unpack old data will cause unpack() return false, which will tell the framework to re-initialize the parameters to default values.

Some macros could be utilized* so only one spot in the code has to change when the list of variables to be packed changes.  In the class declaration, 2 macros can be created: one for the version of the data, and one for the values to be stored.
*Though let’s remember that macros are BAD, so this is a purely academic exercise. Right? Right.

classDeclaration()
{
    #define.CurrentVersion(2)

    #localMacro.CurrentList
        startDate,
        endDate,
        isPosted
    #endMacro 
}

If you’re unfamiliar with macros, what they do is fill in exactly what the macro says at compile time.  So whenever #CurrentVersion is found, the compiler fills in 2 in its place.  Wherever #CurrentList exists, the compiler fills in startDate, endDate, isPosted in its place.  So now pack() and unpack() can be changed to look like this:

container pack()
{
    return [#CurrentVersion, #CurrentList];
}
boolean unpack(container _packedValues)
{    
    boolean ret = true;
    int version = conpeek(_packedValues, 1);

    if (version == #CurrentVersion)
    {
        [version, #CurrentList] = _packedValues;
    }
    else
    {
        ret = false;
    } 

    return ret;
}

If multiple old versions need to be supported, unpack() could end up looking like:

boolean unpack(container _packedClass)
{
    int version = RunBase::getVersion(_packedClass); 
    boolean ret = true;

    switch (version)
    {
        case #CurrentVersion:
            [version, #CurrentList] = _packedClass;
            break;
        case 1:
            [version, startDate, endDate] = _packedClass;
            break;
        default:
            ret = false;
            break;
    }
    return ret;
}

As you can see, it’s just a switch on the version number so old data can be handled at least partially.  If you’re curious about the RunBase::getVersion() call, it’s a better (safer) alternative to conpeek(_packedClass, 1).

This is pretty close to what is found in most pack() and unpack() methods.  I hope this helps clear things up.

So yeah, there you go: pack and unpack. Oh yeah, one other thing…

Don’t use containers and macros, kids.

(I mean, unless you’re converting integers to strings like the cool kids do.)

Just say no to containers and macros. If you’re on Dynamics AX 2012 or Dynamics 365 for Finance and Operations, then just use SysOperation framework. Ok? Ok.

Leave a comment