Unloading .NET Assemblies in TestStand


When working with the TestStand .NET Adapter, it is not always possible to unload the referenced assembly or its dependencies. This document explains how a .NET application unloads assemblies and describes situations when you may not be able to unload an assembly from TestStand even using the Unload All Modules functionality.


Application domains provide an isolation boundary for security, reliability, and versioning, and for unloading assemblies. Application domains are typically created by runtime hosts, which are responsible for bootstrapping the common language runtime before an application is run.  Per Microsoft documentation: “There is no way to unload an individual assembly without unloading all of the application domains that contain it.”  Thus, in the common case of a .NET application that uses only the default application domain, it is not possible to unload any assembly without terminating the application. To support unloading of .NET code module assemblies, TestStand uses additional application domains to load the assemblies referenced via the TestStand .NET Adapter.


.NET Assemblies Referenced at Edit Time

At edit time, the TestStand .NET Adapter creates a new application domain, “TestStandDotNetAdapterConfigurationAppDomain,” anytime it needs to interact with code module assemblies to populate the controls in the step settings panel.  Once TestStand has obtained the necessary information, it unloads this application domain immediately, unloading any assemblies loaded with it. The Unload All Modules functionality is not required in this case.

Unloading the TestStandDotNetAdapterConfigurationAppDomain does not necessarily unload all the assemblies loaded by the .NET adapter, however. If an assembly is also loaded in another application domain, it will remain loaded even after the TestStandDotNetAdapterConfigurationAppDomain has been unloaded. For example, the TestStand Sequence Editor uses mscorlib, so mscorlib is loaded in the default application domain at launch. If you configure a .NET adapter step to call into mscorlib, TestStand loads mscorlib into the TestStandDotNetAdapterConfigurationAppDomain as usual, however mscorlib will remain loaded in the default application domain even after the TestStandDotNetAdapterConfigurationAppDomain has been unloaded.


.NET Assemblies Referenced During Execution

At execution time, the TestStand Engine creates a new application domain, “TSExecutionAppDomain,” to load .NET code module assemblies. TestStand unloads the TSExecutionAppDomain to unload these assemblies. Unlike TestStandDotNetAdapterConfigurationAppDomain, unloading of the TSExecutionAppDomain is not necessarily immediate and depends on a number of factors. It may be necessary to use the Unload All Modules feature to unload the TSExecutionAppDomain. Under certain circumstances, it may not be possible to unload .NET assemblies referenced during execution.


TestStand Object References Keep the TSExecutionAppDomain Alive

TestStand does not unload the TSExecutionAppDomain as long as any TestStand object reference variable refers to an object that exists there. This is true even if you use the Unload All Modules feature. TestStand prevents unloading the TSExecutionAppDomain in this case to avoid the undesirable consequences of unloading an application domain out from underneath a live object reference.

If a reference to the .NET object remains stored in a station global, TestStand will not unload the TSExecutionAppDomain. Because assemblies cannot be unloaded individually, no assemblies loaded by the .NET adapter during execution can be unloaded until the reference is released.

Another way to keep a .NET reference alive, often by accident, is to create a watch expression that references the .NET object. Watch expressions continue to exist as they were last viewed until the next time TestStand pauses or breaks in the execution view. Note: Watch expression lifetime is not tied to executions. Watch expressions persist even after the execution they were last viewed in completes and the execution view is closed.

To release objects referenced by watch expressions, you must pause or break an execution. This will release the old watch expression references. However, if any current watch expressions reference .NET objects, those references now block TestStand from unloading the TSExecutionAppDomain, and must also be released. You can release these references by deleting the watch expressions and continuing the execution. Of course, references to .NET objects held by variables in any current executions must also be released before TestStand will unload TSExecutionAppDomain.

Passing Object Across Application Domains

Another way to prevent an assembly from unloading is to pass a reference to a .NET object from the assembly to another application domain. This causes the .NET runtime to load the assembly in the second application domain, so unloading the TSExecutionAppDomain will not unload the assembly. To unload the assembly, you must also unload the second application domain. If the second application domain is the default application domain, a very common scenario, the assembly cannot be unloaded without terminating the application.

An example of passing objects across application domains is by customizing the C# or Visual Basic versions of the TestStand User Interfaces. A .NET custom user interface can obtain an object reference from the TestStand Engine via the TestStand API and it will load the references into its own application domain. If the reference refers to an object whose type is defined in a .NET code module assembly, that assembly will no longer be unloaded with the TSExecutionAppDomain. For example, a custom user interface might display information about currently running executions that includes data from .NET code modules.

Of course, it is also possible to pass objects across application domains within a .NET code module or custom sequence analyzer module by explicitly creating a new application domain.


Additional Notes: Unload All Modules

Even if you are not passing .NET object references across application domains or storing them in a station global, TestStand does not unload .NET assemblies immediately during execution or at execution termination. There are a variety of reasons for this, but the two most prominent are caching and process models.

To improve performance, TestStand caches certain objects, including sequence files and executions. Cached objects can contain references to .NET objects, preventing TestStand from unloading the TSExecutionAppDomain. The standard TestStand process models contain .NET adapter steps. During execution, even if there are no references to .NET objects held within the client sequence file, there may be references held by the process model. Note also that process model files commonly execute even when you are not running UUTs. For example, selecting “Result Processing…” from the Configure menu executes the “Configure Result Processing” sequence in the current station model.

For these reasons and others, TestStand provides the Unload All Modules functionality. This functionality attempts to release references to modules held by steps in all currently loaded sequence files, including those in the cache. It also attempts to unload modules from the process model sequence files. If all references are successfully released, TestStand will unload the TSExecutionAppDomain.

You can invoke Unload All Modules from the File menu in the TestStand Sequence Editor or User Interface. Alternatively, you can invoke it through the TestStand API by calling Engine.UnloadAllModules(). Note that if you invoke the Engine.UnloadAllModules() method from a TestStand execution, that execution itself may contain references that prevent unloading of the TSExecutionAppDomain and associated .NET assemblies.

Engine.UnloadAllModules() API vs. Unload All Modules Menu Item

The Engine.UnloadAllModules() API method is not identical to the Unload All Modules menu item in the TestStand Sequence Editor or User Interface. The Unload All Modules menu item does not invoke Engine.UnloadAllModules() directly. Instead, it passes the UnloadAllModules CommandKind to the TestStand Application Manager. The application manager in turn invokes Engine.UnloadAllModules() as well as releasing references held by the manager itself. Furthermore, the application (e.g. custom user interface) may subscribe to the Application Manager’s PreCommandExecute or PostCommandExecute events, and perform additional custom cleanup when the UnloadAllModules CommandKind is executed. Thus, in some scenarios, the Unload All Modules menu item unloads .NET assemblies that the Engine API method does not.

Note: The UnloadAllModules CommandKind is disabled if there are any currently running executions.

One common scenario where you must use the Unload All Modules menu item is after using the TestStand Sequence Analyzer from within the TestStand Sequence Editor. The sequence editor hosts an in-process instance of the analyzer. When analyzing a sequence, the analyzer loads modules from contained .NET adapter steps. The engine is unaware of the analyzer instance hosted by the sequence editor, so invoking Engine.UnloadAllModules() does not release references held by the analyzer. The Unload All Modules menu item releases these references because the sequence editor subscribes to the PostCommandExecute event, and releases references held by the analyzer in response to the UnloadnAllModules CommandKind.


Was this information helpful?