Versioning guide: implementing versioning for your changes
Versioning can be implemented in one or two ways, depending on the situation:
- by adding the required information to a
Versioning_XX.json
file; and/or - by adding a
PreviousVersion
attribute to your changed method.
The choice of the appropriate one depends on the change you are doing, as explained in detail in the following sections.
Supported changes
BHoM Versioning supports:
-
Changes to methods (e.g. saved in a script):
- changes in the method name
- changes in their input/outputs names and types.
-
Changes to Namespaces:
- renaming namespaces
- general modification to namespaces
-
Changes to classes (object types):
- changes in class properties
- changes in their name
- complex structural changes
-
Changes to Datasets:
- renaming or moving of location
- deletion
Head to the section below that is the most relevant to your case.
Changes to methods
This section addresses how to do Versioning for code changes done to methods, which are probably the most common. There are two possibilites here, and the first is simpler and to be preferred. Both options apply to either method renamings and/or changes in method inputs.
Via the PreviousVersion
attribute
We recommend to simply add a PreviousVersion
attribute on top of the method you are modifying. This attribute takes two arguments:
- The first argument of the attribute is the current version of BHoM, e.g.
6.1
. - The second argument is the method's Versioning key, obtainable as explained in its dedicated section.
Example: PreviousVersion
for a method renaming
In this example, a method whose full name was FilterFamilyTypesOfFamily
, located in the namespace BH.Engine.Adapters.Revit
and hosted under the static class Create
, is renamed to FilterTypesOfFamily
.
Versioning using the PreviousVersion
attribute for a method being renamed
public static partial class Create
{
[PreviousVersion("3.2", "BH.Engine.Adapters.Revit.Create.FilterFamilyTypesOfFamily(BH.oM.Base.IBHoMObject)")]
[Description("Creates an IRequest that filters Revit Family Types of input Family.")]
[Input("bHoMObject", "BHoMObject that contains ElementId of a correspondent Revit element under Revit_elementId CustomData key - usually previously pulled from Revit.")]
[Output("F", "IRequest to be used to filter Revit Family Types of a Family.")]
public static FilterTypesOfFamily FilterTypesOfFamily(IBHoMObject bHoMObject)
{
//....
}
Example: PreviousVersion
for a method's inputs change
In this example, a method inputs are being changed: an input (the second one) is being removed.
The method in the example is a constructor, but the same example applies to any method. Constructors are rarely used in BHoM – we prefer Create
Engine methods, which get exposed to UIs – but some types, in particular BHoM_Adapter
implementations, make use of them.
Versioning using the PreviousVersion
attribute for a method whose inputs are being changed
public partial class XMLAdapter : BHoMAdapter
{
[PreviousVersion("3.2", "BH.Adapter.XML.XMLAdapter(BH.oM.Adapter.FileSettings, BH.oM.XML.Settings.XMLSettings)")]
[Description("Specify XML file and properties for data transfer")]
[Input("fileSettings", "Input the file settings to get the file name and directory the XML Adapter should use")]
[Input("xmlSettings", "Input the additional XML Settings the adapter should use. Only used when pushing to an XML file. Default null")]
[Output("adapter", "Adapter to XML")]
public XMLAdapter(BH.oM.Adapter.FileSettings fileSettings = null)
{
//....
}
Via the versioning json file
This alternative is trickier and not required in most cases.
The way to do it is to provide a Method
section in the VersioningXX.json
file:
- Add a
VersioningXX.json
file to the project, if it does not yet exists for the current version of BHoM, as explained here. - Create a Versioning key as explained here.
- Get a representational string of the method, like this. If you are changing a constructor method, just leave the
methodName
input empty. - Add the following to the
Method
section of theVersioningXX.json
file, as shown in the below example; make sure to place your changing method's Versioning key and representational string.
Versioning using the Versioning.json
file for a method whose inputs are being changed
"Method": {
"ToNew": {
"BH.Adapter.XML.XMLAdapter(BH.oM.Adapter.FileSettings, BH.oM.XML.Settings.XMLSettings)": {
"_t": "System.Reflection.MethodBase",
"TypeName": "{ \"_t\" : \"System.Type\", \"Name\" : \"BH.Adapter.XML.XMLAdapter, XML_Adapter, Version=3.0.0.0, Culture=neutral, PublicKeyToken=null\" }",
"MethodName": ".ctor",
"Parameters": [
"{ \"_t\" : \"System.Type\", \"Name\" : \"BH.oM.Adapter.FileSettings\" }"
]
}
},
"ToOld": {
}
}
Changes to namespaces
This applies to the case where an entire namespace is renamed. This means all the elements inside that namespace will now belong to a new namespace.
To record that change:
- Add a
VersioningXX.json
file to the project, if it does not yet exists for the current version of BHoM, as explained here. - provide the old namespace as key and the new namespace as value to the
Namespace.ToNew
section of the json file.
In order to make the change backward compatible (i.e. to allow downgrading, i.e. to open a newer BHoM script from a machine running an older version of BHoM), you can fill the ToOld
section with mirrored information.
Change in namespace
Changes to objects
Changes to class name
Modifying the name of a type (i.e. of a class, an object's type) requires to:
- add a
VersioningXX.json
file to the project, if it does not yet exists for the current version of BHoM, as explained here. - add versioning information to the Versioning json file: provide the full name of the old type (namespace + type name) as key and the full name of the new type as value.
In order to make the change backward compatible (i.e. to allow downgrading, i.e. to open a newer BHoM script from a machine running an older version of BHoM), you can fill the ToOld
section with mirrored information.
In the example below, we show how the Versioning json file looks like for two classes being renamed, respectively from DocumentBuilder
to GBXMLDocumentBuilder
and from XMLSettings
to GBXMLSettings
.
Adding information to the Versioning.json
file regarding two classes being renamed
{
...
"Type": {
"ToNew": {
"BH.oM.XML.Settings.XMLSettings": "BH.oM.External.XML.Settings.GBXMLSettings",
"BH.oM.XML.Environment.DocumentBuilder": "BH.oM.External.XML.GBXML.GBXMLDocumentBuilder"
},
"ToOld": {
"BH.oM.External.XML.Settings.GBXMLSettings": "BH.oM.XML.Settings.XMLSettings",
"BH.oM.External.XML.GBXML.GBXMLDocumentBuilder": "BH.oM.XML.Environment.DocumentBuilder"
}
}
}
Changes to class property names
For the case where an object type (i.e. class) was only modified by renaming some of its property, we have a simple solution relying on the Versioning json file. It requires to:
- add a
VersioningXX.json
file to the project, if it does not yet exists for the current version of BHoM, as explained here. - add versioning information to the Versioning json file under the
Property.ToNew
entry. As a key, provide the full name of the type that contains the property you are renaming (namespace + type name) followed by the old property name. The value must be the new property name.
In order to make the change backward compatible (i.e. to allow downgrading, i.e. to open a newer BHoM script from a machine running an older version of BHoM), you can fill the ToOld
section with mirrored information.
In the following example, two properties of the object Bar
that lives in the namespace BH.oM.Structure.Elements
are being renamed repectively from StartNode
to Start
and from EndNode
to End
.
Changes in an object's property names
Structural changes to a a class
What if you completely redesigned a type of object and changed the properties that define it?
This case cannot be solved by a simple replacement of a string and will most likely require some calculations to go from the old object to the new one. This means we need a method that takes the old object in and return the new. This fact presents two challenges:
- The old object definition will not exist anymore, so we cannot use that as the input of the conversion method. Instead we will use a Dictionary containing the properties for both input and output of that conversion method. The other benefit is that the upgrader will not have to depend on BHoM dlls to be able to do the conversion.
- The conversion method needs to be compile and the upgrader needs to be able to access it. While there are ways to keep the conversion method decentralised, it is way simpler to have it in the versioning toolkit directly. This means this is the only case where you cannot just write the upgrade from your own repo. Luckily, this case is less frequent than the others.
So what do you need to do to cover the upgrade?
- First, locate the
Converter.cs
file int the project of the current upgrader. - In that file, write a conversion method with the following signature:
public static Dictionary<string, object> UpgradeOldClassName(Dictionary<string, object> old)
. - In the
Converter
constructor, add that method to theToNewObject
Dictionary. the key is that object type full name (namespace + type name) and the value is the method. - If you want to cover backward compatibility, you can also write a
DowngradeNewClassName
method and add it to theToOldObject
dictionary.
Here's an example.
Structural changes to an object
public class Converter : Base.Converter
{
/***************************************************/
/**** Constructors ****/
/***************************************************/
public Converter() : base()
{
PreviousVersion = "";
ToNewObject.Add("BH.oM.Versioning.OldVersion", UpgradeOldVersion);
}
/***************************************************/
/**** Private Methods ****/
/***************************************************/
public static Dictionary<string, object> UpgradeOldVersion(Dictionary<string, object> old)
{
if (old == null)
return null;
double A = 0;
if (old.ContainsKey("A"))
A = (double)old["A"];
double B = 0;
if (old.ContainsKey("B"))
B = (double)old["B"];
return new Dictionary<string, object>
{
{ "_t", "BH.oM.Versioning.NewVersion" },
{ "AplusB", A + B },
{ "AminusB", A - B }
};
}
/***************************************************/
}
A few things to notice:
- You are working from a Dictionary so make sure that the properties exist before using them
- You will also need to cast them since the dictionary values are all objects
- Make sure to provide the new object type in the dictionary by defining the "_t" property.
Changes to Datasets
Changes to a Dataset name or location
Updating the path to a Dataset works in a similar manner to changes to names of types. The path to a dataset is changed the path from C:\ProgramData\BHoM\Datasets leading up to the json file has been changed in any way. This could be for example be one or more of the following:
- The name of the json file has been changed
- The name of the folder or any super-folder of the json file has been changed
- An additional folder has been added to the path
- A folder has been removed from the path
When this has happened, you will need to:
- add a
VersioningXX.json
file to the project, if it does not yet exists for the current version of BHoM, as explained here. - add versioning information to the Versioning json file under the
Dataset
entries, as shown in the example below.
In the example below, the Versioning json file specifies the move of some structural material files to a parent folder called Structure
.
Changes in a Dataset name or location
{
"Dataset": {
"ToNew": {
"Materials\\MaterialsEurope\\Concrete": "Structure\\Materials\\MaterialsEurope\\Concrete",
"Materials\\MaterialsEurope\\Rebar": "Structure\\Materials\\MaterialsEurope\\Rebar",
"Materials\\MaterialsEurope\\Steel(Grade)": "Structure\\Materials\\MaterialsEurope\\Steel(Grade)",
"Materials\\MaterialsEurope\\Steel": "Structure\\Materials\\MaterialsEurope\\Steel",
"Materials\\MaterialsUSA\\Concrete": "Structure\\Materials\\MaterialsUSA\\Concrete",
"Materials\\MaterialsUSA\\Steel": "Structure\\Materials\\MaterialsUSA\\Steel",
},
"ToOld": {
"Structure\\Materials\\MaterialsEurope\\Concrete": "Materials\\MaterialsEurope\\Concrete",
"Structure\\Materials\\MaterialsEurope\\Rebar": "Materials\\MaterialsEurope\\Rebar",
"Structure\\Materials\\MaterialsEurope\\Steel(Grade)": "Materials\\MaterialsEurope\\Steel(Grade)",
"Structure\\Materials\\MaterialsEurope\\Steel": "Materials\\MaterialsEurope\\Steel",
"Structure\\Materials\\MaterialsUSA\\Concrete": "Materials\\MaterialsUSA\\Concrete",
"Structure\\Materials\\MaterialsUSA\\Steel": "Materials\\MaterialsUSA\\Steel",
}
}
}
When versioning Dataset the ToNew
segment is required, and not optional. This is for the BHoM_UI to be able to update components linking to the Dataset.
The ToOld
versioning of Dataset is optional, but should be done if the developer wants to ensure that the Dataset still is acessible from the same serach paths as before, for calls to the methods in the Library_Engine to still work. This could for example be to ensure the call BH.Engine.Library.Libraries("Materials\\MaterialsEurope\\Concrete")
still returns the same Dataset as before the change was made. It is strongly recomended that calls like the above from code is updated at the same time as the change to the dataset is made, but generally recomended that the ToOld
versioning is done to ensure calls from any UI and that code calls to the methods outside the control of the developer making the change is still functions as before.
Deletion of a Dataset
When a dataset is removed without a replacement, a message should be provided, similar to how it is done for objects and methods. For datasets this is done via the MessageForDeleted section of the Dataset part of the upgrade. Example below showcasing a case where the European concrete and rebar materials have been removed:
Removed Dataset
{
"Dataset": {
"ToNew": {
},
"ToOld": {
}
"MessageForDeleted": {
"Materials\\MaterialsEurope\\Concrete": "Clear message why this dataset has been removed. Point of contact (could be a github repository) where the user can ask questions about why this was removed.",
"Materials\\MaterialsEurope\\Rebar": "Clear message why this dataset has been removed. Point of contact (could be a github repository) where the user can ask questions about why this was removed.",
}
}
}
Items that cannot be versioned: deletions or foundational changes
In some cases, an upgrade/downgrade of a method or object is simply not possible:
- The item was deleted without replacement
- A replacement exists but is so different from the original that an automatic conversion is impossible.
In such cases, it is important to inform the user and provide them with as much information as possible to facilitate the transition to the new version of the code. You will need to:
- add a
VersioningXX.json
file to the project, if it does not yet exists for the current version of BHoM, as explained here. - add versioning information to the Versioning json file under the
MessageForDeleted
and/orMessageForNoUpgrade
entries. As shown in the example below.
Items that cannot be versioned
{
...
"MessageForDeleted": {
"BH.oM.Adapters.DIALux.Furnishing": "This object was provided to build up DIALux models within a BHoM UI, but was deemed to be unnecessary with the suitable conversions between existing Environmental objects and DIALux provided by the DIALux Adapter. To avoid confusion, this object has been removed. If further assistance is needed, please raise an issue on https://github.com/BHoM/DIALux_Toolkit/issues",
"BH.Engine.Grasshopper.Compute.IRenderMeshes(BH.oM.Geometry.IGeometry, Grasshopper.Kernel.GH_PreviewMeshArgs)": "The method was made internal to the Grasshopper Toolkit. If you still need to render objects, consider using one of the Render methods from BH.Engine.Representation instead",
"BH.Engine.Adapters.Revit.Query.Location(BH.oM.Adapters.Revit.Elements.ModelInstance)": "This method was a duplicate of GetProperty method, please use the latter instead.",
"BH.Engine.BuildingEnvironment.Convert.ToConstruction(BH.oM.Base.CustomObject)": "This method was providing a highly specific conversion between a specific custom data schema and Environment Materials that is no longer relevant to the workflows provided in Environments. It is advised to create materials manually using the Solid or Gas types as appropriate. For more assistance please raise an issue for discussion on https://github.com/BuroHappoldEngineering/BuildingEnvironments_Toolkit/issues",
},
"MessageForNoUpgrade": {
"BH.oM.Structure.Loads.BarVaryingDistributedLoad": "The object has been redefined in such a way that automatic versioning is not possible. To reinstate the objects you could try exploding the CustomObject that will have been returned and make use of the BH.Enigne.Structure.Create.BarVaryingDistributedLoadDistanceBothEnds method from the Structures_Engine. If doing this, treat DistanceFromA as startToStartDistance and DistanceFromB as endToEndDistance. Also, treat ForceA and MomentA as ForceAtStart and MomentAtStart, and ForceB and MomentB as ForceAtEnd and MomentAtEnd. If you have any issues with the above, please feel free to raise an issue at https://github.com/BHoM/BHoM_Engine/issues.",
"BH.Engine.Reflection.Modify.SetPropertyValue(System.Collections.Generic.List<BH.oM.Base.IBHoMObject>, System.Type, System.String, System.Object)": "Please use BH.Engine.Reflection.Modify.SetPropertyValue(object obj, string propName, object value) instead.",
"BH.Engine.Base.Compute.Hash(BH.oM.Base.IObject, System.Collections.Generic.List<System.String>, System.Collections.Generic.List<System.String>, System.Collections.Generic.List<System.String>, System.Collections.Generic.List<System.Type>, System.Int32)": "This method's functionality has changed deeply with respect to an older version of BHoM. Please replace this component with BH.Engine.Base.Query.Hash(), then plug the inputs as needed.",
"BH.Engine.Adapters.Revit.Create.ViewPlan": "This method is not available any more. To reinstate the object, please use BH.Engine.Adapters.Revit.Create(string, string) instead.",
"BH.oM.LifeCycleAssessment.MEPScope": "This object has been updated to include new features to enhance calculations for LifeCycleAssesment workflows. Please update the object on the canvas using the default create component to update this component. For further assistance, please raise an issue on https://github.com/BHoM/LifeCycleAssessment_Toolkit/issues",
}
}
Obtaining a Versioning Key
A versioning key is like a signature identifying a method or object.
You can obtain it by using the BH.Engine.Versioning.VersioningKey()
method, as explained below.
Important: get the versioning key before the change
You need to get the versioning key of the object/method before it was changed.
If you have already done your code changes, no worries: you can simply commit your changes on your branch, then switch back to the develop
branch and recompile, then use the BH.Engine.Versioning.VersioningKey()
as explained below.
Get the key for objects
Use the method BH.Engine.Versioning.VersioningKey()
and just provide the input declaringType
, which is the Full Name of the object that you are modifying (i.e. the name of the class preceded by its namespace).
Get the Versioning key for objects
Get the key for methods
Use the method BH.Engine.Versioning.VersioningKey()
and provide both:
- the input
declaringType
, which is the Full Name of the Query/Compute/Create/Modify/Convert class (i.e. the name of the class, preceded by its namespace) which contains the method that you are modifying; - the input
methodName
, which is the name of the method that you are modifying (in case you are renaming the method, this needs to be its name before the rename).
Get the Versioning key for methods
Adding a Versioning_XX.json
file to the project
Adding a Versioning_XX.json
file to the project is needed for certain versioning scenarios, but not all. In some cases (e.g. changes in a method) it may be sufficient to use the PreviousVersion
attribute.
This is as simple as adding an empty json file to the project, named Versioning_XX.json
, where the XX
must be replaced with the current BHoM version. For example:
The empty file should then be immediately populated with the following content (copy-paste it!):
{
"Namespace": {
"ToNew": {
},
"ToOld": {
}
},
"Type": {
"ToNew": {
},
"ToOld": {
}
},
"Property": {
"ToNew": {
},
"ToOld": {
}
},
"MessageForDeleted": {
},
"MessageForNoUpgrade": {
}
}
Then you can fill it in as described by the relevant "changes" section.
Why having a Versioning_XX.json
file?
BHoM Versioning is implemented via a specific, stand-alone mechanism, hosted in the Versioning_Toolkit.
By adding a Versioning_XX.json
file, the information related to code changes are stored locally in each project where the change occurred. This enables decentralisation, i.e. many people can independently code and change BHoM objects or methods in different Toolkits without the need to modify the Versioning_Toolkit, avoiding clashes.
Troubleshooting on Versioning
The upgrade doesn't happen - How can I debug ?
The upgrader are independent exe files so you cannot reach them by attaching to your UI process as you would normally do when debugging the BHoM. They are also hidden processed so you don't have command windows popping up when opening old scripts. In case, you need to figure out what is going on in there, you can always have those upgrade processes visible by commenting two lines of code in the Versioning_Engine (situated on the code BHoM_Engine
repo):
- In the Versioning-Engine project, find the
ToNewVersion
file - In that file, find the
GetPipe
method - Toward the end of that method, comment out the following line:
You should now have command windows popping up as soon as the upgrader are needed. You should also find the BHoMUpgrader processes in your task manager.