Introduction
There are many resources available for starting a Grasshopper (Gh) and Dynamo (Dyn) solution; however, when it comes to deployment (both internally and externally), there are few resources, and what we can find is scattered across various forum discussions. One day, while Braden Koh and I were having a sandwich, we decided to start this series to help address many of these issues, potentially.
We aim to gather and share what we have learned from our individual experiences in developing, deploying, and distributing internal Gh and Dyn libraries within corporate environments.
We aim to share both our joys and frustrations (which I’m sure many can relate to) to highlight valuable lessons that could benefit future developers. Additionally, we aim to shed light on the complexities and challenges that are often overlooked in communities.
These are the articles we plan to write:
- Setting the project, monorepo, multirepo, multi-framework! (this article)
- Building the solution, dependency hell.
- How do we test the Dyn and Gh nodes?
- Development using GitFlow or Trunk.
- Deployment and distribution – GitHub actions, Yak and package manager.
- Measuring and security – capturing data, authentication with internal services!
Before starting, if you don’t know Braden, you should check out his newsletter. He has other interesting articles on computational development in the AEC space.
Setting the project, monorepo, multirepo, multi-framework!
In this era, when ML, LLM, agents, protocols, and vibe coding are drastically changing and reshaping the way we interact with consumer products, discussing desktop applications seems antiquated and irrelevant. Although it may be soon, until we fully transition everything, including every single program, to the web, this post will still be relevant. Even as we go through the technical implementations, most of these decisions tend to be driven by companies’ setup, technical maturity, and collaboration maturity.
Currently, we’re still in transition. Autodesk has moved to.net8
, and Rhino is also in the middle of a transition. With Rhino 7 on Framework4.8 (.net48)
and Rhino 8 on, a hybrid of.net8
or .net48
. Autodesk’s shift was drastic, whereas Rhino 8 is still flexible, allowing you to choose your framework. The catch is that even as software evolves, projects and internal upgrades move much more slowly, often necessitating the need to maintain older versions. The lifetime of any construction project can go from 1 to 15 years. You still have folks working with Revit versions 2022-23, while new ones are being introduced in 2024 or possibly 2025, which is still considered “new,” despite the release of 2026 in April.
So, if you’re looking to set up a plugin or library for Dyn and Gh, you’re looking at maintaining at least six versions (Revit 22, 23, 24, and 25, plus Rhino 7 and 8) and two different frameworks (.net48
, .net8
). On top of that, you’ll also need to think about how you want to organise the development.
You could opt for a monolithic approach, where everything is in one place, but that might make it challenging to manage Gh, Dyn, and all those versions and frameworks. Alternatively, you could split Dyn and Gh into separate repositories. This way, you’d have a Core repo for all the shared logic, a separate one for the UI, and another for project documentation. It can get complicated, and whether you decide to keep it open for contributions from citizen developers depends on the internal company decisions and how confident and skilled your team is.
This article will describe the monolithic solution organised to deploy Gh and Dyn packages, supported by a Core
and CoreUI
project for general functionality and a testing project to test the general functionality and definitions.
Because we need to target different frameworks and versions, we are going to leverage build.props
and build.target
, which lets us control the behaviour of the projects.
The shared Core library is the only project that will use the Standard2.0
, letting other projects (regardless of .net4.8
or .net8
) reference Core
. The rest will switch accordingly to the program
The project is going to look like this:
Project
→ Dyn folder
→→ ZeroTouch node project – net48 or net8
→→ Explicit node project –
net48 or net8
→→ Functions project (solution serving the explicit node) –
net48 or net8
→→ Extension project –
net48 or net8
→→ UI project (specific UI implementation) –
net48 or net8
→ Gh folder
→→ Nodes project (this could be one or multiple) – net48 or net7
→→ UI project (specific UI implementation) –
net48 or net7
→ Core project – Standard2.0
→ CoreUI project – net48, net7 or net8
→ Test project – net48 or net8
→ Web page folder (a static webpage for the documentation)
Why does CoreUI
combine all three frameworks? Because .netStandard
does not include any WPF API. You must dual-target if you need to use dependency properties, dispatchers, elements, or other similar features.
The project Nodes could be multiple, based on your interest in creating and maintaining separate modules for Gh, giving the user the option to choose which package to install. Perhaps you have structures, geometries, utilities, civil engineering, and other related fields.

The power of conditions
The Condition attribute in MSBuild
is super handy for making your build process more flexible and aware of different environments. It lets you set properties, item groups, or targets only when certain things are true, like which version you’re working with (DebugRevit24, DebugDynCore, DebugGhNet7, etc.). In the build.props
and build.target
files, using Condition helps keep version-specific paths organised, loads the right Dynamo or Revit assemblies, and sets output folders based on where you’re targeting. This way, you avoid repeating yourself, keep your project files organised, and ensure the correct settings are applied only when needed.
Understanding build.prop
s and build.targets
When working on a C# project, especially one that interacts with various software versions, such as Revit and Civil3D, managing all the build settings can become messy quickly. That’s where build.props
and build.targets
come in – they’re like the backstage crew that make sure everything runs smoothly without you having to think about it.
These two files act as central control panels for your entire solution. Instead of setting the same properties in each project file, you define them once and MSBuild
automatically apply them to all projects.
build.props
– Setting the stage
Think of build.props
as the setup before the show starts. This file:
- Sets the framework version – It tells all projects to start with
net48
with the latest C# language features. - Configures basic build properties – Like making sure we build for x64 platforms only
- Defines output paths – Where your compiled files should go
- Sets up conditional compilation – Adding special instructions for different types of builds (like Dynamo vs Civil or Grasshopper)
The cool thing is you define this stuff once, and it applies everywhere – no more updating different project files when you want to change the target framework!
<Project>
<PropertyGroup>
<Platforms>x64</Platforms>
<TargetFramework>net48</TargetFramework>
<AssemblyInformationalVersion></AssemblyInformationalVersion>
<AssemblyVersion></AssemblyVersion>
<OutputType>Library</OutputType>
<LangVersion>latest</LangVersion>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<GenerateResourceUsePreserializedResources>true</GenerateResourceUsePreserializedResources>
<ContinuousIntegrationBuild>false</ContinuousIntegrationBuild>
<Nullable>enable</Nullable
<OutputPath>bin\</OutputPath>
</PropertyGroup>
<!-- general debugging condition -->
<PropertyGroup Condition="$(Configuration.Contains('Debug'))">
<ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch>None</ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch>
<Optimize>false</Optimize>
<DebugType>full</DebugType>
<DebugSymbols>true</DebugSymbols>
</PropertyGroup>
<!-- general setting to versioning the generated asseblies -->
<Choose>
<When Condition="'$(AssemblyInformationalVersion)' == '' ">
<PropertyGroup>
<Version>0.0.0.0</Version>
<AssemblyInformationalVersion>0.0.0.0</AssemblyInformationalVersion>
</PropertyGroup>
</When>
<When Condition="'$(AssemblyInformationalVersion)' != '' ">
<PropertyGroup>
<AssemblyProduct>ToolName</AssemblyProduct>
<Version>$(AssemblyVersion)</Version>
</PropertyGroup>
</When>
</Choose>
</Project>
build.props
– Setting the stage
While build.props
sets things up, build.targets
is where the action happens. This file:
- Cleans up old builds – It removes previous builds, ensuring you don’t have mixed files.
- It sets up different Autodesk software environments – Notice all those sections for Revit/Civil3D 2022, 2023, 2024, and 2025? It’s configuring the correct versions of Dynamo and other dependencies for each.
- Manages package references – It automatically pulls in the correct version of Dynamo packages based on which software version you’re targeting
- Copies files to the right places – After building, it organises all your DLLs, PDFs, and other files into a neat package structure
The file has smart conditions (all those Condition=“…” attributes) that only apply settings when needed, so your Revit 2023 build doesn’t confuse settings for Civil3D 2025.
Why does this approach rock?
- Version management made easy – Need to support a new version of Revit? Add a new section in the .targets file with the correct Dynamo version.
- Consistent builds – All team members have the same build settings.
- Simplified project files – Your actual project files can stay clean and minimal
Configuration-specific behaviour: Each tool/version combination (e.g., DebugRevit25, DebugGhNet7, DebugCivil23) activates the correct logic.
<Project>
<!-- Dynamo settings -->
<!-- The IsDynamo and IsCivil properties will be explained in the next article, but you can already have an idea of how they will be used! -->
<PropertyGroup Condition="'$(IsDynamo)' == 'true'">
<ImplicitUsings>enable</ImplicitUsings>
<!-- Some references are stored in the project because
are not distributed as nuget packages -->
<DynamoLibPath>..\..\_dynamoFoRevitLibs</DynamoLibPath>
</PropertyGroup>
<!-- Revit and Civil3D 2023 Setup -->
<!-- Civil3D 2023.2 Setup Dynamo 2.16 -->
<PropertyGroup Condition="'$(IsDynamo)' == 'true' AND ($(Configuration.Contains('R23')) OR $(Configuration.Contains('C23')))">
<DynamoVersion>2.16</DynamoVersion>
<SoftwareVersion>2023</SoftwareVersion>
<DefineConstants>$(DefineConstants);R23</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(IsDynamo)' == 'true' AND ('$(Configuration)' == 'DebugR23' OR '$(Configuration)' == 'DebugC23')">
<StartAction>Program</StartAction>
<!-- Conditional StartProgram for Revit -->
<StartProgram Condition="'$(IsCivil)' == 'false'">C:\Program Files\Autodesk\Revit 2023\Revit.exe</StartProgram>
<!-- Conditional StartProgram for Civil3D -->
<StartProgram Condition="'$(IsCivil)' == 'true'">C:\Program Files\Autodesk\AutoCAD 2023\acad.exe</StartProgram>
<StartArgument Condition="'$(IsCivil)' == 'true'">/ld "C:\Program Files\Autodesk\AutoCAD 2023\AecBase.dbx" /product C3D /language en-US</StartArgument>
</PropertyGroup>
<!-- We repeat the same block of code for all the version required -->
<!-- Revit and Civil3D 2024 - 2025 and for Dynamo Sandbox -->
<!-- We referecence all the common Dynamo package here -->
<!-- We dynamically load the references using the version selected and using the wildcard, which will pick the last release based on the version passed. -->
<!-- Other note, we always exclude the reference from the build at runtime -->
<ItemGroup Condition="'$(IsDynamo)' == 'true'">
<PackageReference Include="DynamoVisualProgramming.Revit" Version="$(DynamoVersion).*">
<ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup Condition="'$(IsDynamo)' == 'true'">
<Reference Include="PythonNodeModels">
<!-- In this way, you can load other references that are not provided with a NuGet.
Alternatively, you could create a NuGet and use and distribute it internally to your organisation. -->
<HintPath>$(DynamoLibPath)\$(DynamoVersion)\PythonNodeModels.dll</HintPath>
<Private>false</Private>
</Reference>
</ItemGroup>
</Project>
As we said, these files manage the general logic and behaviours. Then, for each project, you manage specific conditions or references.
For example, we reference the specific package that only the projects used to build the ZeroTouch
Explicit
nodes require when they are used to construct these nodes.
<!-- Only for Revit -->
<ItemGroup Condition="$(IsCivil) == 'false' AND '$(Configuration)' != 'DebugDynCore'">
<PackageReference Include="Nice3point.Revit.Api.RevitAPI" Version="$(SoftwareVersion).*">
<ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>
<PackageReference Include="Nice3point.Revit.Api.RevitAPIUI" Version="$(SoftwareVersion).*">
<ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>
</ItemGroup>
<!-- Only for Civil3D -->
<ItemGroup Condition="$(IsCivil) == 'true'">
<PackageReference Include="Chuongmep.Civil3D.Api.accoremgd" Version="$(SoftwareVersion).*">
<ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>
<!-- Add here all the required Civil3D packages -->
</ItemGroup>
And then for the Grasshopper project
<Project Sdk="Microsoft.NET.Sdk">
<!-- Changing the framework for Rhino 8 -->
<PropertyGroup Condition="$(Configuration.Contains('GhNet7'))">
<TargetFramework>net7.0-windows</TargetFramework>
</PropertyGroup>
<!-- Rhino 7 settings -->
<PropertyGroup Condition="$(Configuration.Contains('GhNet48'))">
<GrasshopperVersion>7</GrasshopperVersion>
<!-- Set the Grasshopper version used into your organisation -->
<GrasshopperPackage>7.32.23221.10241</GrasshopperPackage>
<StartAction>Program</StartAction>
<StartProgram>C:\Program Files\Rhino 7\System\Rhino.exe</StartProgram>
</PropertyGroup>
<!-- Rhino 8 settings -->
<PropertyGroup Condition="$(Configuration.Contains('GhNet7'))">
<GrasshopperVersion>8</GrasshopperVersion>
<GrasshopperPackage>8.13.24317.13001</GrasshopperPackage>
<StartAction>Program</StartAction>
<StartProgram>C:\Program Files\Rhino 8\System\Rhino.exe</StartProgram>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Grasshopper" Version="$(GrasshopperPackage)">
<ExcludeAssets> runtime </ExcludeAssets>
</PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<!-- All your packages -->
</ItemGroup>
</Project>
This sets the stage for the overall solution! See you in the next section.