Host App Backwards Compatibility

I could use some guidance on version compatibility when developing nodes that rely on host app DLLs. I’ve been developing for AutoCAD/Civil 3D 2022 so that I’m working with the latest and greatest, but I also want to be able to maintain backwards compatibility to v2020 as best I can (or at least throw helpful exceptions if a method is not found). In Visual Studio I’ve created references to the necessary AutoCAD and Civil 3D DLLs in C:\Program Files\Autodesk\AutoCAD 2022. Does this mean that none of the nodes will work on a machine that has an older version installed since I haven’t referenced anything in the AutoCAD 2021 or AutoCAD 2020 folders? Or will most nodes work minus those that rely on new methods introduced in 2022? I was thinking I could just test my package with v2020, but then that wouldn’t really be a correct test unless I completely uninstalled v2022, right?

I’m sure this is similar when working with the Revit APIs. Any guidance appreciated.

Hi @mzjensen

I would suggest If you build your custom nodes on older version(2020) that might work for newer also most cases or you can add if version is below 2020 do this and above 2020 do that.

Hope it helps!

If I create references to the DLLs in the 2020 folder, how do I know that it will work for a user that only has 2022 installed? Seems like the path would point to a folder that doesn’t exist. And I can’t add multiple references in VS for the same assembly name but different versions.

Try it. See what you get and come back here if you get any exceptions.

I appreciate it @Kulkul, but I’m looking for a little more detail here. There has to be a better way to do this besides uninstalling/reinstalling different versions and testing one at a time.

@Thomas_Mahon are you willing to share any tips for how you handle this with Bimorph?

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

@Thomas_Mahon superb, thank you very much for the thorough response. I’ll have to tweak things a bit to fit my specific needs, but this gives me a really great starting point, so I’ll consider it the solution for now. Like you mentioned, it sounds like this could be a management struggle as the number of deprecated methods increases, which I wouldn’t be surprised to discover once I create the build configs. The Civil 3D .NET API is updated pretty regularly as they are still migrating it over from the old COM API. So I am surprised to hear that you are only needing to deal with 3 methods! Are those methods that exist for Revit 2022 and don’t exist in prior versions, or did they exist in prior versions and have since been flagged as obsolete?

No problem, yeah if there are lots of depreciated methods in your package then its unlikely that my approach will be suitable.

In answer to you question, yep all of them are depreciated methods; mainly the change from UnitType to ForgeTypeId had the greatest impact. I’ve also kept BimorphNodes intentionally small which subsequently reduces the number of dependences on any particular library which in-turn reduces the risk of running into depreciated API calls. I guess there is also a bit of fortune at play since none of the other API calls I need to make have been changed nor been marked as depreciating since Revit 2018.

I just read Konrad’s recent post about Shared Projects in VS, which sounds like a great approach as well. I’ll try that out as well as different build configs and see what works best.

Hi @Thomas_Mahon, question on this step. Not sure if it’s the same for Revit, but for Civil 3D the DLLs are named the same between versions so I can’t add more than one as a reference in Visual Studio. Do you have any guidance?

Capture

Hi @mzjensen You have to create the Compatibility project as a separate project in your solution.

E.g.:
image

Then in the Compatibility project reference the older Civils dlls. You would then have another project in your solution, e.g. BimorphNodes.Nodes (your ZT node project) which references the latest version you want to support. If you have multiple versions of Civils you want to support then you will need to add a compatibility project to your solution for each one.

OK, that’s what I was wondering. When I first read your outline I thought you were somehow making it work with a single compatibility project. Thanks for clarifying!

1 Like

No problem, just remember this is where this technique will get unwieldly if you have multiple compatibility projects as you will also have to increase your if statements which is going to get ugly. I’m lucky in that respect with BimorphNodes as I only need 1 compatibility project as the breaking changes are covered by either Revit 2022 API or 2019 API and this covers me for 2018-2022 so I only need a ternary plus its called in only a handful of places so I can be lazy about it.

If I had to support more than two versions of the Revit API (so more than 1 compatibility project) I would get rid of the if statements entirely and use polymorphism but this would significantly change how you would go about architecting your solution.