Innovative Revit Add-in Development (part 3)

Innovative Revit Add-in Development (part 3)

Third-party libraries are often used to save time and money when developing Revit add-ins. However, when multiple add-ins reference different versions of the same library, version conflicts can occur (DLL hell).

Innovative Revit Add-in Development (part 1)
Innovative Revit Add-in Development (part 2)

In the third part of my three-part blog series on "Innovative Revit Add-in Development", I will show you how to avoid version conflicts when using third-party libraries by using add-in isolation.

With the release of Revit 2025, support for .NET8 was introduced. I had hoped that Revit would take advantage of the new capability to load add-ins within their own contexts. Unfortunately, as soon as I launched my first test add-in with .NET8, I received the following error message:

Understanding version conflicts

Version conflicts occur when two or more add-ins reference the same assemblies but require different versions of those assemblies. For example, if an older version of an assembly is loaded, but the add-in requires types from the newer version, a TypeLoadException will be thrown. Similar errors can occur if methods are not found, or their parameters have changed.

The previous image shows a ````TypeLoadExceptionthrown when accessing theMicrosoft.Extensions.Options``` assembly. Revit had loaded the assembly in version 7.0, but my test add-in required version 8.0 of the same assembly.

AssemblyLoadContext

In .NET4.8, assemblies were loaded within a process in an AppDomain. To avoid version conflicts, some add-in frameworks created a separate AppDomain for each add-in and loaded the add-in assemblies within that AppDomain. However, sharing data between AppDomains is not straightforward; the transfer objects must either be serializable or derived from MarshallByRefObject. In both cases, sharing large amounts of data takes a considerable amount of time. This was almost certainly one reason why this was not offered in earlier versions of Revit.

Starting with .NET6, there is now exactly one instance of an AppDomain within a process, and AppDomains are no longer supported for isolating assemblies. Instead, AssemblyLoadContext instances are used. Each AssemblyLoadContext instance represents a unique scope for assembly instances and the type definitions they contain. Unlike AppDomains, there is no binary isolation between these dependencies, so objects can be freely exchanged between contexts.

Important Note: If two AssemblyLoadContext instances contain type definitions with the same name, they are not the same type. They are only the same type if they come from the same assembly instance.

Resolving Assemblies

Normally, assemblies are loaded from the application directory into the default AssemblyLoadContext. However, in add-in frameworks (such as Revit), assemblies are typically located in separate directories where the application cannot find them. In order for Revit to load an add-in into the default context, it needs a reference to the directory where the associated assemblies are located. This requires specifying the type of Revit application and the path to the assembly in the *.addin manifest:

<RevitAddIns>
	<AddIn Type="Application">
		<Name>Test</Name>
		<FullClassName>Scotec.Revit.Test.RevitTestApp</FullClassName>
		<Assembly>.\Scotec.Revit.Test\Scotec.Revit.Test.dll</Assembly>
		<AddInId>F2E1648B-7E1A-4518-95E9-92437EA941A6</AddInId>
		<VendorId>scotec</VendorId>
		<VendorDescription>scotec Software Solutions AB</VendorDescription>
	</AddIn>
</RevitAddIns>

However, referenced assemblies that are in the same add-in directory are not automatically loaded. .NET first checks to see whether an assembly with the same name has already been loaded. If so, that assembly is used, even if the version referenced by the add-in is different from the loaded version. If the assembly is not already loaded, .NET looks for it in the application directory. If it is not found there, the default context fires a resolving event. Add-ins can register an event handler for this event, which can then load the assembly.

Unfortunately, there is a problem with this: If more than one event handler is registered for this event, the event handlers are called in sequence until one event handler returns a nonzero value. Subsequent event handlers are then ignored. Therefore, the version of an assembly that is loaded depends on the order in which the add-ins are loaded.

Isolation - a separate load context for each add-in

To isolate add-ins from each other, they can be loaded in their own load contexts. Each load context can load its own assembly instances - and thus its own versions of the assemblies.

To create a new context, derive a new class from AssemblyLoadContext and override the Load method. When an assembly referenced by your add-in needs to be loaded, the Load method is the first opportunity to determine where the assembly should be loaded from.

The search for the assemblies is carried out in the following order:

  • Calling the AssemblyLoadContext.Load method.
  • Checking the cache of the AssemblyLoadContext.Default instance.
  • Executing the default validation logic in the default context. When an assembly is reloaded, a reference to the new assembly instance is added to the cache of the default instance.
  • Triggering the AssemblyLoadContext.Resolving event for the active AssemblyLoadContext.
  • Triggering the AppDomain.AssemblyResolve event.

Important note: Revit always creates instances of your Revit app classes or Revit commands in the default context. You therefore need to ensure that method calls to these instances are processed in the appropriate context.

A simple and efficient way to run Revit add-ins in their own AssemblyLoadContext is provided by the open-source component Scotec.Revit.Isolation, which can be loaded as a NuGet package.

Once you have loaded the Scotec.Revit.Isolation NuGet package in the package manager, assign the [RevitApplicationIsolation] attribute to your app class:

[RevitApplicationIsolation]
public class RevitTestApp : IExternalApplication
{
	public RevitTestApp()
	{
		var context = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly());
	}
	...
}

The [RevitApplicationIsolation] attribute causes a RevitApplication factory to be automatically created during code compilation. This factory is responsible for creating a new AssemblyLoadContext for the add-in and instantiating the Revit App within it. The second and final step is to register this factory instead of the app class in the *.addin manifest by adding the Factory postfix to the class name.

<RevitAddIns>
	<AddIn Type="Application">
		<Name>Test</Name>
		<FullClassName>Scotec.Revit.Test.RevitTestAppFactory</FullClassName>
		<Assembly>.\Scotec.Revit.Test\Scotec.Revit.Test.dll</Assembly>
		<AddInId>F2E1648B-7E1A-4518-95E9-92437EA941A6</AddInId>
		<VendorId>scotec</VendorId>
		<VendorDescription>scotec Software Solutions AB</VendorDescription>
	</AddIn>

In the code example above, I am determining the current AssemblyLoadContext in which the assembly is running within the RevitTestApp class constructor for testing purposes. Without the [RevitApplicationIsolation] attribute, calling the GetLoadContext method returns the default context. When the attribute is used, an add-in specific context is returned.

You can see which assemblies have actually been loaded in the Module view in Visual Studio. There you can see all the assemblies loaded in the process, along with their paths to the add-in directory.

The following image shows the loaded assemblies of a Revit process without using isolation. The Microsoft.Extensions.Options.dll assembly was loaded exactly once in Version 7.0.

The next list shows the loaded assemblies when using isolation via AssemblyLoadContext. The Microsoft.Extensions.Options.dll assembly has now been loaded a total of three times: once by Revit in Version 7.0 and once individually by two add-ins in Version 8.0.

Revit Command and CommandAvailability

Revit also creates instances of other classes in your add-in, such as commands. Again, Revit creates the instances in the default context. So, we follow the same procedure as for the Revit app. The class RevitTestCommand receives the attribute [RevitCommandIsolation] and the class RevitTestCommandAvailability receives the attribute [RevitCommandAvailabilityIsolation].

[RevitCommandIsolation]
[Transaction(TransactionMode.Manual)]
public class RevitTestCommand : IExternalCommand
{
    ...
}

[RevitCommandAvailabilityIsolation]
public class RevitTestCommandAvailability : IExternalCommandAvailability
{
    ...
}

When registering, we then append the Factory postfix to the class name, as we did previously with the Revit app. In the following code, I will demonstrate this using a push button as an example.

private static PushButtonData CreateButtonData(string name, string text, ...)
{
	return new PushButtonData(name
		, text
		, Assembly.GetExecutingAssembly().Location
		, typeof(RevitTestCommandFactory).FullName)
		{
			...
			AvailabilityClassName = typeof(RevitTestCommandAvailabilityFactory).FullName
		};
}



Isolating Revit add-ins using AssemblyLoadContext provides an elegant and straightforward solution to avoiding DLL hell. The Scotec.Revit.Isolation library assists you in this process by automatically generating the required code when compiling your add-in. The factories created in this process then generate instances of your apps or commands in the add-in specific load context and ensure that Revit calls are forwarded accordingly.



NuGet: Scotec.Revit.Isolation
GitHub: scotec-revit



Innovative Revit Add-in Development (part 1)
Innovative Revit Add-in Development (part 2)

An error has occurred. This application may no longer respond until reloaded. An unhandled exception has occurred. See browser dev tools for details. Reload 🗙