ILRepack and Assembly Embedding
This document describes how DBTools merges and embeds assemblies to achieve a minimal deployment footprint while avoiding conflicts with other Revit add-ins.
Overview
DBTools uses two complementary strategies to manage dependencies:
| Strategy | Framework | Purpose |
|---|---|---|
| ILRepack | net48 only | Merges most dependencies INTO DBTools.dll |
| Embedding | Both TFMs | Embeds assemblies as resources, loaded on-demand |
Both strategies aim to:
- Minimize deployment files (ideally just
DBTools.dll+DBTools.Loader.dll+ WPF theme DLLs) - Avoid conflicts with other Revit add-ins that may load different versions of shared libraries
- Prevent GAC/Revit-provided assemblies from being used instead of our versions
Source:
build/BuildMerging.cs:11-22
Why ILRepack? The Revit Add-in Conflict Problem
Revit add-ins share a single AppDomain (net48) or AssemblyLoadContext (net8). When multiple add-ins depend on the same library (e.g., Serilog, Newtonsoft.Json), the first-loaded version wins.
Problems this causes:
- Type identity mismatches (your code expects
Serilog 4.0, butSerilog 2.0was loaded first) - Missing method exceptions (newer API called on older assembly)
- Mysterious crashes when reflection fails
ILRepack's solution:
- Merge dependencies INTO
DBTools.dllwith internalized types - Types become
internalto your assembly, invisible to other add-ins - No conflict possible because types aren't shared
Source:
build/BuildMerging.cs:17-21
net48 vs net8.0-windows Strategy Differences
net48 (Revit 2024)
Uses both ILRepack merging AND embedding:
+-------------------+
| DBTools.dll |
| (ILRepack'd) | <-- Contains merged types from Serilog, M.E.*, etc.
+-------------------+
|
+-- Embedded resources: ricaun.Revit.UI.*.dll (loaded from bytes)
|
+-- Separate files: DBTools.HandyControl.dll, DBTools.Themes.dll, etc.
net8.0-windows (Revit 2025/2026)
Uses embedding only (no ILRepack):
+-------------------+
| DBTools.dll |
| (standard) |
+-------------------+
|
+-- Embedded resources: ALL dependencies (DBTools.Core, Serilog, M.E.*, etc.)
|
+-- Separate files: DBTools.HandyControl.dll, DBTools.Themes.dll, etc.
Why no ILRepack for net8?
- .NET 8's
AssemblyLoadContextprovides natural isolation - Each add-in can have its own isolated context
- ILRepack has compatibility issues with some .NET 8 assemblies
Source:
build/BuildMerging.cs:13-14,src/DBTools.Loader/AssemblyResolution/EmbeddedAssemblyResolver.cs:32-37
ILRepack Merging (net48)
Target: MergeNet48Assemblies
The build target MergeNet48Assemblies performs the merge:
Target MergeNet48Assemblies => _ => _
.Description("Merge ALL dependencies into DBTools.dll for net48 via ILRepack (except WPF themes)")
.DependsOn(FlattenYearOutputs)
.After(BuildApp, BuildTests)
.Before(PromoteToDist)
Source:
build/BuildMerging.cs:23-29
What Gets Merged (Everything Except Whitelist)
The strategy is "merge everything EXCEPT a small whitelist":
var keepSeparate = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"DBTools.dll", // Primary assembly (merging INTO this)
"DBTools.Loader.dll", // Entry point loaded by Revit
"DBTools.HandyControl.dll", // WPF theme - needs file Location
"DBTools.Themes.dll", // WPF theme - needs file Location
"Microsoft.Extensions.Logging.Abstractions.dll", // Shared with sandbox
// Revit API (shouldn't be in output)
"RevitAPI.dll", "RevitAPIUI.dll", "AdWindows.dll", "UIFramework.dll"
};
Source:
build/BuildMerging.cs:50-65
Prefix-Based Exclusions
Some assemblies are excluded by prefix (because NuGet adds version numbers):
var keepSeparatePrefixes = new[] { "ricaun.Revit.UI" };
Why ricaun assemblies aren't merged:
- ILRepack corrupts ricaun IL, causing runtime crashes
- Instead, ricaun assemblies are EMBEDDED as resources
Source:
build/BuildMerging.cs:67-70
Internalization with Exclusions
ILRepack internalizes merged types, but some namespaces must stay public:
var excludeFile = yearDir / "ilrepack-exclude.txt";
File.WriteAllText(excludeFile, "^DBTools\\.Core\\.\n^Serilog\\.\n^Microsoft\\.Extensions\\.\n^Microsoft\\.Bcl\\.\n^System\\.\n^ricaun\\.Revit\\.");
| Namespace | Why Public |
|---|---|
DBTools.Core.* |
Base classes used by tools |
Serilog.* |
Reflection-based sink discovery |
Microsoft.Extensions.* |
DI and configuration |
Microsoft.Bcl.* |
Polyfill interfaces (IAsyncDisposable) |
System.* |
Polyfill types |
Source:
build/BuildMerging.cs:114-123
ILRepack Command
The actual merge command:
args.Append($"/internalize:\"{excludeFile}\" "); // Internalize with exclusions
args.Append("/allowdup "); // Handle duplicate polyfill types
args.Append("/ndebug "); // Skip debug info
args.Append($"/lib:\"{revitPath}\" "); // Resolve Revit API references
args.Append($"/out:\"{dbToolsDll}\" "); // Output to DBTools.dll
args.Append($"\"{backupOriginal}\" "); // Primary assembly
// + all assemblies to merge
Source:
build/BuildMerging.cs:129-144
Assembly Embedding Strategy
Build Target: DBT_EmbedCopyLocalAssemblies
The embedding happens during MSBuild via a custom target:
<Target Name="DBT_EmbedCopyLocalAssemblies"
AfterTargets="ResolveReferences"
Condition="'$(DesignTimeBuild)'!='true'">
Source:
src/DBTools.App/DBTools.App.csproj:236-238
Resource Naming Convention
Embedded assemblies use a logical name pattern:
DBTools.EmbeddedAssemblies.{AssemblyName}.dll
For example:
DBTools.EmbeddedAssemblies.Serilog.dllDBTools.EmbeddedAssemblies.DBTools.Core.dll
<ItemGroup>
<_EmbedLogical Include="@(_EmbedIdentity->'DBTools.EmbeddedAssemblies.%(Name).dll')">
<SourcePath>%(OriginalItemSpec)</SourcePath>
</_EmbedLogical>
<EmbeddedResource Include="%(_EmbedLogicalDistinct.SourcePath)"
LogicalName="%(_EmbedLogicalDistinct.Identity)" />
</ItemGroup>
Source:
src/DBTools.App/DBTools.App.csproj:260-267
Exclusions from Embedding
Revit-provided assemblies (never copy/embed):
<_EmbedCandidate Remove="@(_EmbedCandidate)"
Condition="'%(Filename)%(Extension)' == 'RevitAPI.dll'" />
<_EmbedCandidate Remove="@(_EmbedCandidate)"
Condition="'%(Filename)%(Extension)' == 'Newtonsoft.Json.dll'" />
WPF resource assemblies (need file Location):
<_EmbedCandidate Remove="@(_EmbedCandidate)"
Condition="$([System.String]::Copy('%(Filename)').StartsWith('DBTools.Themes'))" />
Source:
src/DBTools.App/DBTools.App.csproj:241-251
Runtime Assembly Resolution
EmbeddedAssemblyResolver
The resolver is installed early during add-in startup:
public static void Install(Assembly mainAssembly, string? deployedDir = null)
{
// ...
#if NET8_0_OR_GREATER
var alc = AssemblyLoadContext.GetLoadContext(mainAssembly) ?? AssemblyLoadContext.Default;
alc.Resolving += (context, name) => ResolveNet8(context, mainAssembly, name);
#else
AppDomain.CurrentDomain.AssemblyResolve += (_, args) => Resolve(mainAssembly, args.Name);
#endif
}
Source:
src/DBTools.Loader/AssemblyResolution/EmbeddedAssemblyResolver.cs:24-38
Resolution Flow (net48)
Assembly requested (e.g., "Serilog")
|
v
1. Already loaded in AppDomain?
YES -> Return existing (prevents duplicates)
|
v
2. Try embedded resource: "DBTools.EmbeddedAssemblies.Serilog.dll"
FOUND -> Assembly.Load(bytes)
|
v
3. Check if merged via ILRepack (TryGetMergedAssembly)
MERGED -> Return mainAssembly
|
v
4. DBTools.* namespace? Try file-based fallbacks
FOUND -> Assembly.LoadFrom(path)
|
v
5. Return null (let CLR handle)
Source:
src/DBTools.Loader/AssemblyResolution/EmbeddedAssemblyResolver.cs:40-95
Merged Assembly Detection (net48)
For ILRepack'd assemblies, the resolver returns the main assembly:
private static Assembly? TryGetMergedAssembly(Assembly mainAssembly, string requestedName)
{
// WPF themes NOT merged
if (requestedName.Equals("DBTools.HandyControl", ...) ||
requestedName.Equals("DBTools.Themes", ...) ||
requestedName.Equals("DBTools.Loader", ...))
return null;
// DBTools.Core is merged
if (requestedName.Equals("DBTools.Core", ...))
return mainAssembly;
// Other merged deps
if (requestedName.StartsWith("Serilog", ...) ||
requestedName.StartsWith("Microsoft.Extensions", ...) ||
requestedName.Equals("YamlDotNet", ...))
return mainAssembly;
return null;
}
Source:
src/DBTools.Loader/AssemblyResolution/EmbeddedAssemblyResolver.cs:225-257
Resolution Flow (net8)
private static Assembly? ResolveNet8(AssemblyLoadContext alc, Assembly mainAssembly, AssemblyName requested)
{
// 1. Already loaded? Return it
var alreadyLoaded = alc.Assemblies.FirstOrDefault(...);
if (alreadyLoaded != null) return alreadyLoaded;
// 2. Try embedded resource
var resourceName = ResourcePrefix + requestedName + ".dll";
using var stream = mainAssembly.GetManifestResourceStream(resourceName);
if (stream != null)
{
return alc.LoadFromStream(ms); // Load into same ALC
}
// 3. File-based fallbacks for WPF assemblies
return TryLoadFromFileFallbacksNet8(alc, mainAssembly, requestedName);
}
Source:
src/DBTools.Loader/AssemblyResolution/EmbeddedAssemblyResolver.cs:142-185
WPF Theme Assemblies: Why They Stay Separate
The pack:// URI Problem
WPF's XAML parser uses pack:// URIs to locate resources within assemblies:
<ResourceDictionary Source="pack://application:,,,/DBTools.HandyControl;component/Themes/Theme.xaml" />
The parser uses Assembly.Location to resolve these URIs. Assemblies loaded from byte arrays have Location = "", breaking resource lookup.
Source:
src/DBTools.Loader/AssemblyResolution/EmbeddedAssemblyResolver.cs:93-94,src/DBTools.App/DBTools.App.csproj:244-251
Which Assemblies Need File Location
| Assembly | Reason |
|---|---|
DBTools.HandyControl |
Contains BAML for HandyControl widgets |
DBTools.Themes |
Contains merged theme dictionaries |
Source:
build/BuildMerging.cs:54-57
File-Based Resolution Fallbacks
For WPF assemblies, the resolver tries file-based loading:
private static Assembly? TryLoadFromFileFallbacks(Assembly mainAssembly, string requestedName)
{
// 1. Check %APPDATA%/DBTools/vendor/<type>/<tfm>/
var appDataPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"DBTools", "vendor", vendorType, vendorTfm, dllName);
if (File.Exists(appDataPath))
return Assembly.LoadFrom(appDataPath);
// 2. Check deployed directory (next to DBTools.Loader.dll)
return TryLoadFromDeployedDir(deployedDir, dllName);
}
Source:
src/DBTools.Loader/AssemblyResolution/EmbeddedAssemblyResolver.cs:101-129
Preloading Critical Assemblies (net48)
The GAC Problem
On net48, AssemblyResolve only fires when an assembly CAN'T be found. If an older version exists in the GAC or was loaded by another add-in, the CLR uses it instead of asking our resolver.
Solution: Preload During Startup
Before any code uses these assemblies, DBTools.Loader preloads them from embedded resources:
#if !NET8_0_OR_GREATER
PreloadEmbeddedConfigurationAssemblies(assembly);
PreloadEmbeddedSerilogAssemblies(assembly);
#endif
Source:
src/DBTools.Loader/Addin/AddinEntry.cs:31-34
Configuration Assembly Preload
private static void PreloadEmbeddedConfigurationAssemblies(Assembly mainAssembly)
{
var configAssemblies = new[]
{
"Microsoft.Extensions.Primitives",
"Microsoft.Extensions.FileProviders.Abstractions",
"Microsoft.Extensions.FileProviders.Physical",
"Microsoft.Extensions.Configuration.Abstractions",
"Microsoft.Extensions.Configuration",
"Microsoft.Extensions.Configuration.FileExtensions",
"Microsoft.Extensions.Configuration.Json",
"Microsoft.Extensions.Configuration.Binder"
};
foreach (var name in configAssemblies)
{
var alreadyLoaded = GetLoadedAssemblyBySimpleName(name);
if (alreadyLoaded != null) continue;
var resourceName = "DBTools.EmbeddedAssemblies." + name + ".dll";
using (var stream = mainAssembly.GetManifestResourceStream(resourceName))
{
if (stream == null) continue;
using (var ms = new MemoryStream())
{
stream.CopyTo(ms);
Assembly.Load(ms.ToArray());
}
}
}
}
Source:
src/DBTools.Loader/Addin/AddinEntry.cs:131-165
Serilog Assembly Preload
Critical: Dependencies must be loaded BEFORE dependents:
var serilogAssemblies = new[]
{
// 1. Base abstractions first
"Microsoft.Extensions.Logging.Abstractions",
"Microsoft.Extensions.DependencyInjection.Abstractions",
// 2. M.E.Logging (depends on Abstractions)
"Microsoft.Extensions.Logging",
// 3. Serilog core
"Serilog",
// 4. Serilog extensions (depend on above)
"Serilog.Extensions.Logging",
"Serilog.Enrichers.Thread",
"Serilog.Sinks.File"
};
Source:
src/DBTools.Loader/Addin/AddinEntry.cs:176-192
Forbidden Host Assemblies
Build-Time Guardrails
The build prevents certain assemblies from landing in output:
<ItemGroup>
<ForbiddenHostAssembly Include="RevitAPI.dll" />
<ForbiddenHostAssembly Include="RevitAPIUI.dll" />
<ForbiddenHostAssembly Include="AdWindows.dll" />
<ForbiddenHostAssembly Include="UIFramework.dll" />
<ForbiddenHostAssembly Include="Newtonsoft.Json.dll" />
<ForbiddenHostAssembly Include="System.Runtime.dll" />
<ForbiddenHostAssembly Include="System.ObjectModel.dll" />
</ItemGroup>
Source:
src/DBTools.App/DBTools.App.csproj:171-179
Automatic Cleanup
A build target removes forbidden assemblies if transitive dependencies copied them:
<Target Name="DBT_RemoveForbiddenAfterCopy"
AfterTargets="CopyFilesToOutputDirectory"
BeforeTargets="FailIfHostAssembliesInOutput">
<Delete Files="@(_DBT_ForbiddenToClear)" />
</Target>
Source:
src/DBTools.App/DBTools.App.csproj:181-198
Build Failure on Violation
If any forbidden assemblies remain after cleanup, the build fails:
<Target Name="FailIfHostAssembliesInOutput">
<Error Text="Forbidden host assemblies detected in output: @(_ForbiddenInOutput)."
Condition="'@(_ForbiddenInOutput)' != ''" />
</Target>
Source:
src/DBTools.App/DBTools.App.csproj:200-213
Summary: What Goes Where
| Assembly Type | net48 | net8.0-windows | Location |
|---|---|---|---|
| DBTools.Core | ILRepack merged | Embedded resource | Inside DBTools.dll |
| Serilog.* | ILRepack merged | Embedded resource | Inside DBTools.dll |
| M.E.DI/Config | ILRepack merged | Embedded resource | Inside DBTools.dll |
| YamlDotNet | ILRepack merged | Embedded resource | Inside DBTools.dll |
| ricaun.Revit.* | Embedded resource | Embedded resource | Inside DBTools.dll |
| DBTools.Themes | Separate file | Separate file | Next to DBTools.dll |
| DBTools.HandyControl | Separate file | Separate file | Next to DBTools.dll |
| DBTools.Loader | Separate file | Separate file | Next to DBTools.dll |
| RevitAPI/UI | Never copied | Never copied | Revit provides |
Troubleshooting
Issue: "Type X not found" or "Method Y not found"
Cause: Assembly version mismatch - another add-in loaded a different version first.
Solutions:
- Ensure dependencies are embedded or merged
- Check if the assembly is in
keepSeparatewhitelist unnecessarily - Preload the assembly during startup
Issue: WPF resource not found (pack:// URI failure)
Cause: WPF assembly loaded from bytes instead of file.
Solutions:
- Ensure WPF assemblies are in
keepSeparatewhitelist - Verify DLLs are deployed next to
DBTools.Loader.dll - Check
EmbeddedAssemblyResolverfile fallback paths
Issue: ILRepack crash or corrupted IL
Cause: Some assemblies have IL that ILRepack can't process.
Solutions:
- Add to
keepSeparatePrefixesto exclude from merge - Embed as resource instead of merging
- Check ILRepack version compatibility
Issue: Duplicate type definitions after merge
Cause: Multiple assemblies define the same polyfill type.
Solution: ILRepack /allowdup flag handles this:
args.Append("/allowdup "); // Handle duplicate polyfill types
Source:
build/BuildMerging.cs:131
Related Documentation
- Architecture Overview - High-level system architecture
- Project References - How projects reference each other
- Build Pipeline - Build system details
- DBTools.Loader - Assembly loading at runtime
Verification Status
| Check | Status |
|---|---|
| Source anchors for all claims | Yes |
| UNVERIFIED markers where needed | No (all verified) |
| Cross-references added | Yes |
| Examples tested | N/A |
| No assumptions without evidence | Yes |
Verified by: docs-run-20260124-020412
Date: 2026-01-24