In this article, we will discuss a specific case of using our components in user projects and the potential issues associated with it.
Introduction
The .NET Framework provides mechanisms for running multiple applications within a single process using Application Domains. Each AppDomain is an isolated logical container where a task is executed with its own collection of assemblies. Only one version of an assembly can be loaded into each AppDomain, and while assemblies can’t be unloaded individually, the entire AppDomain can be unloaded.In .NET, only one application domain can be used, but there are Assembly Load Contexts. By default, all assemblies are loaded into the AssemblyLoadContext.Default context, but new contexts can be created to load different versions of assemblies. These contexts themselves can also be unloaded. Important!
Identical types from different Assembly Load Contexts are incompatible. Even if the same assembly is loaded into multiple contexts, the types from these contexts will remain incompatible with each other.
The nature of the problem
Where an assembly is loaded plays a crucial role:- if an assembly is explicitly added to the main application, it’s usually loaded into the default context and becomes accessible to all other assemblies;
- if an assembly is used within another plugin assembly, it’s loaded dynamically and typically into the same context as the plugin itself. This leads to two possible scenarios:
- if the plugin is loaded into the main context, there are no issues;
- if the plugin is loaded into a separate context, problems arise - these will be discussed in detail later.
First problem
The core issue is that some commands "lose" the execution context they belong to. As a result, they can’t access types from that context, leading to incorrect behavior.For example, the command TypeDescriptor.GetConverter(type) refers to a system assembly, which can only be loaded into the default context. During execution, it "forgets" the calling context. If type is located in the calling context, the command fails to retrieve the type and its associated converter. These converters are specified via attributes in certain classes (StiMargin, StiBorder, etc.) and are used during serialization.
Because of this:
- loading reports from XML format fails with an error message: «... TypeConverter cannot convert from System.String»;
- report compilation doesn’t work.
We can’t fix this issue on our side because we don’t call this command directly—it is invoked internally by other system methods.
.NET developers don’t consider this a significant problem and suggest using workarounds instead.
Solutions to the problem
Option 1. Simple but with limitations:
Report loading: you can use an .mrt file in JSON format. This type of loading works differently, so the issue doesn’t occur.
Calculation mode: avoid using compilation as the expression evaluation mode in the report. Instead, set the evaluation mode to Interpretation.
We have also implemented an improvement: if this error occurs, the report's evaluation mode will be automatically switched to Interpretation.
Option 2:
You can wrap calls to our classes in a special command that forces the correct context:
StiReport stiReport;
using (AssemblyLoadContext.EnterContextualReflection(Assembly.GetExecutingAssembly()))
{
stiReport = new StiReport();
stiReport.Load(templateStream);
stiReport.Render(false);
}
Important!The result of TypeDescriptor.GetConverter(type) is cached somewhere inside the system library. If this method is first called without the wrapper, any subsequent calls with the correct wrapper will still return the incorrect, cached result.
Second problem
This issue is similar to the previous one. The command Type.GetType(name) sometimes fails to retrieve the required types.Solutions to the problem
Since this command is used in our code, we have already implemented a fix to address this issue.
Third problem
When compiling a report, its assembly is loaded into a separate context so that it can be unloaded when the report is disposed of. However, previously, this was designed only for cases where our assemblies were loaded into the default context.Solutions to the problem
We have implemented a small improvement, and now this mechanism also works for plugins.