Host App Backwards Compatibility

Hi @mzjensen sure. The technique I use isn’t something I would recommend if I was to be a purist about robust software development practices, but for custom node packages its an acceptable trade-off compared to say, creating different builds or versions of your package which burdens both the consumer and developer of the package.

I would also say the approach I’ve taken works because I only have 3 depreciated methods to support and they are only called a handful of times throughout the entire codebase; if you have more than 10 methods to support or if you are calling methods multiple times then you’ll need to consider carefully if its the right approach.

Here are the steps to take:
Preqrequisites:

  1. Ensure you solution is broken down into separate projects. BimorphNodes for example is comprised of 6 projects: BimorphNodes.Nodes, BimorphNodes.Infrastructure, BimorphNodes.Core etc.
  2. Target the highest version of Civils/AutoCAD you want to support in all of these projects. (So Revit 2022 in my case).
  3. Create separate build configurations in Visual Studio: I have Debug2019 and Debug2022 and then modify your csproj files in each project by duplicating the references to Civils/AutoCAD and any Dynamo libraries, and change them to target the lowest version you want to support, then add the conditional tag to them so you can easily switch between the library versions via your config (@Konrad_K_Sobon also has a blog on this topic too which also addresses your question re backwards compatibility, so check it out for an alternative option):
    <Reference Include="RevitNodes" Condition="'$(Configuration)' == 'Debug2022' Or '$(Configuration)' == 'Release'">
      <HintPath>..\..\..\..\..\..\..\Program Files\Autodesk\Revit 2022\AddIns\DynamoForRevit\Revit\RevitNodes.dll</HintPath>
      <Private>False</Private>
    </Reference>

and the duplicate:

    <Reference Include="RevitNodes" Condition="'$(Configuration)' == 'Debug2019'">
      <HintPath>..\..\..\..\..\..\..\Program Files\Dynamo\Dynamo Revit\2\Revit_2019\RevitNodes.dll</HintPath>
      <Private>False</Private>
    </Reference>

Adding backwards compatibility:

  1. Write your package using the primary build targeting the highest version you are supporting.
  2. Create a new project - e.g. BimorphNodes.Compatibility - and in this project add the references to the older versions of the Civils/AutoCAD and Dynamo APIs you want to target for backwards compatibility. I target R2019 for backwards compatibility and this also provides compatibility for R2018 as the depreciated API calls I need are present in both versions.
  3. After writing your entire package, change the build config to the older version to switch the library references. Visual Studio will then highlight all the unknown API calls you’ve declared from the newer API which are not present in the older one. For each broken API call, write a method in your ‘compatibility’ project which makes the call using the depreciated method.
  4. Write methods in your main project which mirror these methods (see an example from BimorphNodes below).
  5. Declare a property which stores the version number of the active document - BimorphNodes stores this inside a services class which is passed around the codebase as a singleton for easy access.
  6. For each broken API call, replace it with a ternary operator which evaluates the application version number so you can delegate to one of the methods accordingly.
  7. Build your compatibility project and add a reference to it in your main package project.
  8. Switch your build configuration back to the one targeting the latest version.

That’s all there is too it. The build configs are just for quickly seeing where the breaking API calls are and nothing else.

There is one other very important gotcha you need to be aware of; if you don’t wrap your API calls in mirrored methods and simply make the API call (to the newer API) in your ternary, then you’ll get runtime exceptions if that ternary is evaluated while older versions of the application are running your package, so this is a crucial step and you need to make sure that the call is completely masked.

Examples:
I have extensions classes for Autodesk.Revit.DB.Parameter:
This is the 2022 version (which works for R2021):

        public static double ConvertToDisplayUnits(this Parameter parameter)
        {
            var value = parameter.AsDouble();

            var displayValue = UnitUtils.ConvertFromInternalUnits(value, parameter.GetUnitTypeId());

            return displayValue;
        }

And this is the ‘mirrored’ version which exists in the BimorphNodes.Compatibility project that targets the depreciated call for Revit 2019 (and Revit 2020 and Revit 2018):

        public static double ConvertToDisplayUnits2020(this Parameter parameter)
        {
            var value = parameter.AsDouble();

            return UnitUtils.ConvertFromInternalUnits(value, parameter.DisplayUnitType);
        }

Finally, this is how these methods are called in the codebase, where NodeSettings.RevitApiNewUnitsVersion is a constant storing the version number which requires the latest API calls and nodeServices.RevitVersion is the version of the active Revit document:

var displayUnitValue = nodeServices.RevitVersion < NodeSettings.RevitApiNewUnitsVersion
                        ? parameter.ConvertToDisplayUnits2020() 
                        : parameter.ConvertToDisplayUnits();

And as mentioned, the following would cause a runtime exception if a user runs this code in an older version - e.g. Revit 2019 - which is why you need to wrap the API calls in mirrored methods and delegate to one method or the other via a conditional:

var displayUnitValue = nodeServices.RevitVersion < NodeSettings.RevitApiNewUnitsVersion
                        ? parameter.ConvertToDisplayUnits2020() 
                        : UnitUtils.ConvertFromInternalUnits(parameter.AsDouble(), parameter.GetUnitTypeId());

In conclusion, this technique is good if you only make a small number of calls to depreciated methods and is a simple way of managing backwards compatibility without the hassle of maintaining multiple versions. All you need to do is manage your ‘compatibility’ project and the rest of the solution needs only be concerned with the latest versions you want to target.

Conversely, if you have a lot of depreciated methods to support, or if there are lots of breaking changes between annual API versions, then this technique will create too much complexity and become impractical, so chose your approach wisely.

7 Likes