Table of Contents

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:

  1. Minimize deployment files (ideally just DBTools.dll + DBTools.Loader.dll + WPF theme DLLs)
  2. Avoid conflicts with other Revit add-ins that may load different versions of shared libraries
  3. 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, but Serilog 2.0 was loaded first)
  • Missing method exceptions (newer API called on older assembly)
  • Mysterious crashes when reflection fails

ILRepack's solution:

  • Merge dependencies INTO DBTools.dll with internalized types
  • Types become internal to 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 AssemblyLoadContext provides 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.dll
  • DBTools.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:

  1. Ensure dependencies are embedded or merged
  2. Check if the assembly is in keepSeparate whitelist unnecessarily
  3. Preload the assembly during startup

Issue: WPF resource not found (pack:// URI failure)

Cause: WPF assembly loaded from bytes instead of file.

Solutions:

  1. Ensure WPF assemblies are in keepSeparate whitelist
  2. Verify DLLs are deployed next to DBTools.Loader.dll
  3. Check EmbeddedAssemblyResolver file fallback paths

Issue: ILRepack crash or corrupted IL

Cause: Some assemblies have IL that ILRepack can't process.

Solutions:

  1. Add to keepSeparatePrefixes to exclude from merge
  2. Embed as resource instead of merging
  3. 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



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