Table of Contents

DBTools.Loader

Purpose: Revit entry point assembly that bootstraps DBTools.
Output: DBTools.Loader.dll
Target Frameworks: net48, net8.0-windows


Overview

DBTools.Loader is the critical bootstrap component that Revit loads directly via the .addin manifest. Its sole responsibility is to load DBTools.dll (the main application assembly) and install the assembly resolver infrastructure that handles embedded dependencies.

This project is intentionally minimal to reduce startup failure risk. All application logic lives in DBTools.App; the loader exists only to:

  1. Find and load DBTools.dll
  2. Install the EmbeddedAssemblyResolver
  3. Forward Revit lifecycle calls to DBTools.App.AddinEntry

Source: src/DBTools.Loader/Addin/AddinEntry.cs:24


Responsibilities

Responsibility Implementation
Revit entry point IExternalApplication implementation (decorated with [AppLoader] for AppLoader discovery)
Load main assembly LoadMainAssembly() loads DBTools.dll from deployed directory
Install assembly resolver EmbeddedAssemblyResolver.Install() handles runtime resolution
Pre-load critical assemblies net48 only: Configuration and Serilog assemblies pre-loaded to beat GAC
Forward lifecycle OnStartup/OnShutdown delegated to inner DBTools.App.AddinEntry

Source: src/DBTools.Loader/Addin/AddinEntry.cs:17


Key Components

AddinEntry (Revit Entry Point)

The AddinEntry class is the Revit-facing entry point. It implements IExternalApplication and is decorated with [AppLoader] from ricaun.Revit.UI so AppLoader can discover and invoke it. Hot-reload/unload semantics only apply on net8 builds (Revit 2025+).

[AppLoader]
public sealed class AddinEntry : IExternalApplication
{
    private IExternalApplication? _inner;

    public Result OnStartup(UIControlledApplication application)
    {
        // Bootstrap sequence here...
    }
}

Source: src/DBTools.Loader/Addin/AddinEntry.cs:17-20

Key Fields:

  • _inner: Stores the instantiated DBTools.App.AddinEntry for forwarding lifecycle calls

Source: src/DBTools.Loader/Addin/AddinEntry.cs:22

EmbeddedAssemblyResolver

Static class that installs hooks into the CLR's assembly resolution mechanism. Handles loading dependencies from embedded resources within DBTools.dll.

internal static class EmbeddedAssemblyResolver
{
    private const string ResourcePrefix = "DBTools.EmbeddedAssemblies.";
    private static int _installed;
    private static string? _deployedDir;

    public static void Install(Assembly mainAssembly, string? deployedDir = null)
    {
        // Installs AssemblyResolve (net48) or ALC.Resolving (net8) handler
    }
}

Source: src/DBTools.Loader/AssemblyResolution/EmbeddedAssemblyResolver.cs:14-38

LoaderCompat

Provides cross-TFM compatibility utilities for the loader. Abstracts differences between .NET Framework and .NET 8.

internal static class LoaderCompat
{
    internal static string VendorTfm =>
#if NET8_0_OR_GREATER
        "net8.0-windows";
#else
        "net48";
#endif

    internal static Assembly LoadAssemblyFromPath(string path)
    {
#if NET8_0_OR_GREATER
        return AssemblyLoadContext.Default.LoadFromAssemblyPath(path);
#else
        return Assembly.LoadFrom(path);
#endif
    }
}

Source: src/DBTools.Loader/Compat/LoaderCompat.cs:10-35

SharedParametersDeployer

Deploys the embedded shared parameters file to %APPDATA%/DBTools/ on first run. This file defines Revit shared parameters used by DBTools features.

internal static class SharedParametersDeployer
{
    private const string ResourceName = "DBTools.sharedparameters.txt";
    
    public static void EnsureDeployed()
    {
        // Copies embedded resource to %APPDATA%/DBTools/DBTools.sharedparameters.txt
        // Skips if file already exists
    }
}

Source: src/DBTools.Loader/Addin/SharedParametersDeployer.cs:8-49

Behavior:

  • Creates %APPDATA%/DBTools/ directory if needed
  • Skips deployment if file already exists (idempotent)
  • Logs errors to %LOCALAPPDATA%/DBTools/Logs/Loader.log on failure
  • Best-effort: failures don't prevent application startup (installer also deploys this file)

Bootstrap Sequence

OnStartup Flow

The complete startup sequence with line numbers:

1. Validate application parameter
   > Source: AddinEntry.cs:19

2. Get deployed directory (where DBTools.Loader.dll lives)
   > Source: AddinEntry.cs:21

3. Load DBTools.dll from deployed directory
   > Source: AddinEntry.cs:22

4. Verify embedded payload is present
   > Source: AddinEntry.cs:23

5. Install EmbeddedAssemblyResolver
   > Source: AddinEntry.cs:24

6. [net48 only] Pre-load Configuration assemblies
   > Source: AddinEntry.cs:26

7. [net48 only] Pre-load Serilog assemblies
   > Source: AddinEntry.cs:27

8. Resolve DBTools.App.AddinEntry type via reflection
   > Source: AddinEntry.cs:30-31

9. Create instance of DBTools.App.AddinEntry
   > Source: AddinEntry.cs:33-34

10. Store instance and forward OnStartup call
    > Source: AddinEntry.cs:36-37

Source: src/DBTools.Loader/Addin/AddinEntry.cs:17-38

Sequence Diagram

Revit                    DBTools.Loader           DBTools.dll (DBTools.App)
  |                            |                           |
  |--OnStartup(app)----------->|                           |
  |                            |                           |
  |                      GetDeployedDirectory()            |
  |                      LoadMainAssembly()                |
  |                            |--------load-------------->|
  |                            |                           |
  |                      EnsureEmbeddedPayloadPresent()    |
  |                      EmbeddedAssemblyResolver.Install()|
  |                            |                           |
  |                      [net48: PreloadEmbedded*()]       |
  |                            |                           |
  |                      Activator.CreateInstance()------->|
  |                            |                           |
  |                            |------OnStartup(app)------>|
  |                            |                           |
  |<-------Result.Succeeded----|<-------Result-------------|

Assembly Loading Strategy

net48 Strategy (ILRepack + Embedded)

On .NET Framework 4.8, dependencies are handled via two mechanisms:

1. ILRepack (Merged into DBTools.dll):

  • DBTools.Core
  • Serilog, Microsoft.Extensions., YamlDotNet, System. polyfills
  • These types exist directly in DBTools.dll; no resolution needed

2. Embedded Resources (for ricaun.Revit.*):

  • Stored as DBTools.EmbeddedAssemblies.<name>.dll
  • Loaded via Assembly.Load(byte[]) at runtime
  • ILRepack corrupts some IL, so these stay as resources

Source: src/DBTools.Loader/AssemblyResolution/EmbeddedAssemblyResolver.cs:223-263

3. Pre-loaded Assemblies:

Configuration assemblies are pre-loaded before any code uses them to ensure DBTools' versions load before GAC/Revit's conflicting versions:

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"
};

Source: src/DBTools.Loader/Addin/AddinEntry.cs:111-121

Serilog assemblies are also pre-loaded in dependency order:

var serilogAssemblies = new[]
{
    "Microsoft.Extensions.Logging.Abstractions",
    "Microsoft.Extensions.DependencyInjection.Abstractions",
    "Microsoft.Extensions.Logging",
    "Serilog",
    "Serilog.Extensions.Logging",
    "Serilog.Enrichers.Thread",
    "Serilog.Sinks.File"
};

Source: src/DBTools.Loader/Addin/AddinEntry.cs:154-170

Why Pre-load?

On net48, AssemblyResolve only fires when an assembly cannot be found. If the GAC or another add-in already loaded a different version, our resolver never runs. Pre-loading ensures our versions are loaded first.

Source: src/DBTools.Loader/Addin/AddinEntry.cs:104-108

net8.0-windows Strategy (All Embedded)

On .NET 8, all dependencies are embedded as resources and loaded via AssemblyLoadContext:

alc.Resolving += (context, name) => ResolveNet8(context, mainAssembly, name);

Source: src/DBTools.Loader/AssemblyResolution/EmbeddedAssemblyResolver.cs:33-34

Resolution loads from embedded resources via stream:

using var stream = mainAssembly.GetManifestResourceStream(resourceName);
if (stream != null)
{
    using var ms = new MemoryStream();
    stream.CopyTo(ms);
    ms.Position = 0;
    return alc.LoadFromStream(ms);
}

Source: src/DBTools.Loader/AssemblyResolution/EmbeddedAssemblyResolver.cs:168-176

WPF Assembly Fallback

WPF resource assemblies that use pack:// URIs must be loaded from files because URI resolution requires Assembly.Location. In DBTools, this includes DBTools.HandyControl (vendored) and DBTools.Themes.

Search Order:

  1. %APPDATA%/DBTools/vendor/<type>/<tfm>/ (traditional install)
  2. Deployed directory (next to DBTools.Loader.dll)

Source: src/DBTools.Loader/AssemblyResolution/EmbeddedAssemblyResolver.cs:101-129

Vendor Type Detection:

private static string? GetVendorTypeForAssembly(string requestedName)
{
    if (requestedName.Equals("DBTools.HandyControl", ...))
        return "handycontrol";
    return null;
}

Source: src/DBTools.Loader/AssemblyResolution/EmbeddedAssemblyResolver.cs:131-139

Duplicate Load Prevention

Both resolution paths check for already-loaded assemblies first to prevent type identity mismatches:

// Critical for ricaun.Revit.* which AppLoader may have already loaded
var alreadyLoaded = AppDomain.CurrentDomain.GetAssemblies()
    .FirstOrDefault(a => string.Equals(a.GetName().Name, requestedName, ...));
if (alreadyLoaded != null)
    return alreadyLoaded;

Source: src/DBTools.Loader/AssemblyResolution/EmbeddedAssemblyResolver.cs:50-61


Dependencies

NuGet Packages

Package Purpose
ricaun.Revit.UI Provides [AppLoader] attribute for enhanced Revit loading

Source: src/DBTools.Loader/DBTools.Loader.csproj:20

Note: ricaun.Revit.UI is a compile-time dependency that ensures ricaun is loaded EARLY when Revit loads DBTools.Loader.dll. This is required for RevitTaskService to work correctly in DBTools.App.

Source: src/DBTools.Loader/DBTools.Loader.csproj:16-18

Revit API References

Conditional based on target framework:

net48:

<Reference Include="RevitAPI">
  <HintPath>$(REVIT2024_DIR)\RevitAPI.dll</HintPath>
  <Private>false</Private>
</Reference>

Source: src/DBTools.Loader/DBTools.Loader.csproj:23-34

net8.0-windows:

<Reference Include="RevitAPI" Condition="'$(REVIT_NET8_DIR)'!=''">
  <HintPath>$(REVIT_NET8_DIR)\RevitAPI.dll</HintPath>
  <Private>false</Private>
</Reference>

Source: src/DBTools.Loader/DBTools.Loader.csproj:36-47

Runtime Dependencies

  • DBTools.dll - Must be present in same directory as DBTools.Loader.dll

Source: src/DBTools.Loader/Addin/AddinEntry.cs:67-69


Error Handling

Startup Errors

The loader has minimal error handling - it must either succeed or fail fast:

Missing DBTools.dll:

if (!File.Exists(mainPath))
    throw new FileNotFoundException("DBTools.dll not found next to DBTools.Loader.dll.", mainPath);

Source: src/DBTools.Loader/Addin/AddinEntry.cs:68-69

Missing Embedded Payload (net8 only):

var hasCore = Array.Exists(resources, r => 
    string.Equals(r, "DBTools.EmbeddedAssemblies.DBTools.Core.dll", ...));
if (!hasCore)
{
    throw new InvalidOperationException(
        "DBTools.dll is missing embedded dependencies...");
}

Source: src/DBTools.Loader/Addin/AddinEntry.cs:89-96

Type Resolution Failure:

var type = assembly.GetType("DBTools.App.AddinEntry", throwOnError: true)
           ?? throw new InvalidOperationException("DBTools.App.AddinEntry type not found.");

Source: src/DBTools.Loader/Addin/AddinEntry.cs:30-31

Silent Failures (Intentional)

Pre-load failures during net48 startup are logged to Debug output but don't fail startup:

catch (Exception ex)
{
    // Silent continue - the EmbeddedAssemblyResolver will handle on-demand loading
    System.Diagnostics.Debug.WriteLine($"[DBTools.Loader] Preload failed for {name}: {ex.Message}");
}

Source: src/DBTools.Loader/Addin/AddinEntry.cs:192-196

This is acceptable because the EmbeddedAssemblyResolver can still load these assemblies on-demand if pre-loading fails.


Build Configuration

Project Properties

<TargetFrameworks>$(DBT_RevitTargetFrameworks)</TargetFrameworks>
<AssemblyName>DBTools.Loader</AssemblyName>
<RootNamespace>DBTools.Loader</RootNamespace>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>

Source: src/DBTools.Loader/DBTools.Loader.csproj:3-13

CopyLocalLockFileAssemblies: Set to true to copy NuGet dependencies (ricaun) to output for AppLoader/Debug scenarios.

Source: src/DBTools.Loader/DBTools.Loader.csproj:13

Suppressed Warnings

<NoWarn>$(NoWarn);MSB3277</NoWarn>

MSB3277 (assembly version conflicts) is suppressed because Revit host references surface unavoidable conflicts.

Source: src/DBTools.Loader/DBTools.Loader.csproj:10-11


File Structure

src/DBTools.Loader/
+-- DBTools.Loader.csproj
+-- Addin/
|   +-- AddinEntry.cs              # IExternalApplication entry point
|   +-- SharedParametersDeployer.cs # Deploys shared parameters file to %APPDATA%
+-- Assets/
|   +-- DBTools.sharedparameters.txt  # Embedded shared parameters definition
+-- AssemblyResolution/
|   +-- EmbeddedAssemblyResolver.cs   # Runtime assembly loading
+-- Compat/
    +-- LoaderCompat.cs               # Cross-TFM utilities

Integration with AppLoader

DBTools.Loader uses ricaun's [AppLoader] attribute so AppLoader can discover and invoke the loader entry point. On net8 builds (Revit 2025+), AppLoader can additionally provide:

  • Automatic Revit version detection
  • Enhanced error reporting
  • Integration with ricaun.RevitTest for automated testing
  • Hot-reload support on Revit 2025+ when loaded through AppLoader's net8 host

The AppLoader may have already loaded shared assemblies (like ricaun.Revit.*). The resolver explicitly checks for already-loaded assemblies to prevent duplicate loading which would cause type identity mismatches.

Source: src/DBTools.Loader/AssemblyResolution/EmbeddedAssemblyResolver.cs:51-53

Hot-Reload (Revit 2025+ / net8) and AssemblyLoadContext Rules

On Revit 2025+ (net8), AppLoader hot-reload relies on unloading and recreating a collectible AssemblyLoadContext (ALC). For hot-reload to work, DBTools must avoid loading or resolving DBTools.* assemblies/types outside the current ALC (for example, via process-wide AppDomain scans or Assembly.Load(...)).

DBTools enforces this by:

  • Loading the main assembly (DBTools.dll) into the same ALC as DBTools.Loader.dll
  • Loading/looking up DBTools.* assemblies in an ALC-scoped way when resolving tool assemblies or fallback type resolution

Source: src/DBTools.Loader/Addin/AddinEntry.cs:17
Source: src/DBTools.Core/Tools/DbtToolAssemblyLoader.cs:39
Source: src/DBTools.Core/Tools/DbtTypeResolver.cs:35
Source: src/DBTools.App/Bootstrap/AppBootstrapper.cs:331

For development reload stability, point AppLoader at a single-year output directory:

  • .artifacts/dist/Release/2025
  • .artifacts/dist/Release/2026

Do not watch the multi-year parent (.artifacts/dist/Release) because DBTools uses the same add-in identity across years and should only be loaded once per Revit session.

Source: build/BuildTargets.cs:857-860 Source: build/Build.cs:555-599 Source: build/ArtifactManagement.cs:50-76


Troubleshooting

"DBTools.dll not found" Error

Cause: DBTools.dll is not in the same directory as DBTools.Loader.dll.

Solution: Ensure the deployment copies both files together.

"Missing embedded dependencies" Error (net8)

Cause: DBTools.dll was built without the embedded assembly step.

Solution: Rebuild using bash build.sh BuildAll.

Source: src/DBTools.Loader/Addin/AddinEntry.cs:93-96

Assembly Resolution Failures

Symptoms: FileNotFoundException or TypeLoadException for dependency types.

Diagnosis:

  1. Check if the assembly is in embedded resources (DBTools.EmbeddedAssemblies.<name>.dll)
  2. For WPF assemblies, check %APPDATA%/DBTools/vendor/ or deployed directory
  3. Enable assembly binding logging (FUSLOGVW.exe) on net48


Verification Status

Check Status
Source anchors for all claims Yes
UNVERIFIED markers where needed Yes (addin manifest)
Cross-references added Yes
Examples tested N/A
No assumptions without evidence Yes

Verified by: docs-run-20260124-020412
Date: 2026-01-24