Table of Contents

Leveraging DBTools Modularity

DBTools is designed as a modular Revit add-in platform where tools share common infrastructure while maintaining clear boundaries. This guide explains how to leverage the shared services from DBTools.Core and DBTools.Themes to build tools that integrate seamlessly with the platform.

Philosophy

DBTools follows these modularity principles:

  1. Shared Infrastructure, Independent Tools - Core services are centralized; tools are self-contained
  2. Dependency Injection First - All services are resolved through DI, enabling testing and flexibility
  3. Contract-Driven - Interfaces define contracts; implementations can vary per context
  4. Error Handling at Boundaries - All entrypoints run inside ISafeExecutor
  5. Window-Scoped Theming - Each window owns its theme resources to avoid cross-add-in conflicts

Source: src/DBTools.App/Bootstrap/DbtServiceBootstrapper.cs:69-130

Two-Tier DI Architecture

DBTools uses a two-tier dependency injection model:

Tier Lifetime Purpose Examples
Singleton Application lifetime Infrastructure services ISafeExecutor, IAlertService, ISettingsProvider, DbtToolRegistry
Scoped Per-command lifetime Revit context services ITransactionRunner, IRevitCallGate, IRevitRunScope

Source: src/DBTools.App/Bootstrap/DbtServiceBootstrapper.cs:77-79


Shared Services from DBTools.Core

ISafeExecutor

The central error handling service. All tool entrypoints must execute within ISafeExecutor.

public interface ISafeExecutor
{
    Task RunAsync(Func<Task> action, ILogger logger, IErrorNotifier? notifier = null,
        CancellationToken ct = default);
    Task RunAsync(Func<Task> action, ILogger logger, IErrorNotifier? notifier,
        SafeExecutor.SafeExecuteOptions? opts, CancellationToken ct = default);
}

Source: src/DBTools.Core/Execution/ISafeExecutor.cs:6-16

What it provides:

  • Correlation IDs for log tracing
  • Execution timing
  • Exception logging with full stack traces and inner exceptions
  • User notification via banners
  • Automatic debug mode activation on fatal errors
  • Lifecycle hooks (OnSuccessAsync, OnCancelAsync, OnErrorAsync)

Source: src/DBTools.Core/Execution/SafeExecutor.cs:12-467

Usage in a command:

// Commands automatically use ISafeExecutor via DbtToolCommand base class
public sealed class GmCommand : DbtToolCommand
{
    protected override async Task RunAsync(IDbtToolContext context)
    {
        var exec = context.Resolve<ISafeExecutor>();
        
        // For nested async operations that need error handling:
        await exec.RunAsync(async () =>
        {
            // Your work here
        }, context.Logger, context.ErrorNotifier);
    }
}

Source: src/Tools/Common/GM/Features/GmCommand.cs:32-93


ITransactionRunner

Unified API for executing Revit model modifications with automatic transaction management.

public interface ITransactionRunner
{
    Task RunAsync(string name, Action action, CancellationToken ct = default);
    Task<T> RunAsync<T>(string name, Func<T> action, CancellationToken ct = default);
    Task RunAsync(string name, Action<Document> action, CancellationToken ct = default);
    Task<T> RunAsync<T>(string name, Func<Document, T> action, CancellationToken ct = default);
    Task RunAsync(string name, Action<UIApplication, Document> action, CancellationToken ct = default);
    Task<T> RunAsync<T>(string name, Func<UIApplication, Document, T> action, CancellationToken ct = default);
    // Overloads with explicit Document parameter
    Task RunAsync(Document doc, string name, Action<Document> action, CancellationToken ct = default);
    Task<T> RunAsync<T>(Document doc, string name, Func<Document, T> action, CancellationToken ct = default);
}

Source: src/DBTools.Core/Revit/Transactions/ITransactionRunner.cs:6-53

Key behaviors:

  • Auto-selects transaction type: SubTransaction if doc.IsModifiable, otherwise new Transaction
  • Cross-document protection
  • Failure handling with SilentFailuresPreprocessor

Usage example:

public class FoundationTypeOrganizer
{
    private readonly ITransactionRunner _tx;
    
    public FoundationTypeOrganizer(ITransactionRunner tx, Document doc, ILogger logger)
    {
        _tx = tx;
        // ...
    }
    
    public async Task OrganizeAsync()
    {
        await _tx.RunAsync("Organize Foundation Types", doc =>
        {
            // Modify document - transaction is automatic
        });
    }
}

Source: src/Tools/Structural/OrganizeFoundationTypes/Features/FoundationTypeOrganizer.cs:19-27


ITransactionGroupService

Manages transaction groups for operations requiring multiple transactions to appear as a single undo item.

public interface ITransactionGroupService
{
    bool IsActive { get; }
    Task BeginAsync(string name, CancellationToken ct = default);
    Task FinalizeAsync(bool commit, CancellationToken ct = default);
    Task RunAsync(string name, bool commit, Func<Task> work, CancellationToken ct = default);
}

Source: src/DBTools.Core/Revit/Transactions/ITransactionGroupService.cs:3-14

Usage for batch operations:

public class TdvService
{
    private readonly ITransactionRunner _tx;
    private readonly ITransactionGroupService _group;
    
    public TdvService(IRevitCallGate gate, ITransactionRunner tx, 
        ITransactionGroupService group, ILogger<TdvService> logger)
    {
        _tx = tx;
        _group = group;
        // ...
    }
    
    public async Task ApplyMultipleChangesAsync()
    {
        await _group.RunAsync("Batch Apply", commit: true, async () =>
        {
            // Multiple transactions appear as one undo item
            await _tx.RunAsync("Step 1", doc => { /* ... */ });
            await _tx.RunAsync("Step 2", doc => { /* ... */ });
            await _tx.RunAsync("Step 3", doc => { /* ... */ });
        });
    }
}

Source: src/Tools/Common/TDV/Revit/Services/TdvService.cs:20-23


IAlertService

Service for showing alert dialogs with various body types.

public interface IAlertService
{
    AlertResult Show(AlertRequest request);
    bool Confirm(string message, string title = "DB Tools");
    T? SelectSingle<T>(IEnumerable<T> items, Func<T, string> displayFunc,
        string windowTitle, string header, string? message = null,
        AlertWindowOptions? options = null) where T : class;
    IReadOnlyList<T> SelectMultiple<T>(IEnumerable<T> items, Func<T, string> displayFunc,
        string windowTitle, string header, string? message = null,
        AlertWindowOptions? options = null) where T : class;
}

Source: src/DBTools.Core/UI/Alerts/Services/IAlertService.cs:5-26

Usage examples:

// Simple confirmation
var alerts = context.Resolve<IAlertService>();
if (alerts.Confirm("Delete 15 elements?", "Confirm Delete"))
{
    // Proceed
}

// Single selection
var selectedLevel = alerts.SelectSingle(
    levels,
    level => level.Name,
    windowTitle: "Select Level",
    header: "Target Level",
    message: "Choose the level to place elements on");

// Multiple selection
var selectedTypes = alerts.SelectMultiple(
    familyTypes,
    t => t.Name,
    windowTitle: "Select Types",
    header: "Types to Process");

Source: src/DBTools.Core/UI/Alerts/Services/AlertService.cs:51-183


ILogger

DBTools uses Microsoft.Extensions.Logging with Serilog as the provider.

// Resolve from DI
var logger = context.Resolve<ILogger<MyService>>();

// Or use the context's logger
var logger = context.Logger;

// Structured logging
logger.LogInformation("Processing {Count} elements in {Document}", 
    elements.Count, doc.Title);

logger.LogWarning("[GM] Failed to compute DocumentKey: {Message}", ex.Message);

logger.LogError(ex, "[{Command}] execution failed.", GetType().Name);

Source: src/DBTools.App/Bootstrap/DbtServiceBootstrapper.cs:381-386

Important: Always use structured logging with named placeholders, not string interpolation.


IRevitRunScope

Represents a single Revit run scope (command, modeless session, or test). Provides access to the current Revit context.

public interface IRevitRunScope
{
    UIApplication UIApplication { get; }
    IRevitCallGate CallGate { get; }
    ITransactionRunner TransactionRunner { get; }
    ITransactionGroupService TransactionGroupService { get; }
    UIDocument GetActiveUiDocument();
    Document GetActiveDocument();
    Document GetLockedDocument(Document expectedDocument, string? context = null);
}

Source: src/DBTools.Core/Revit/Execution/IRevitRunScope.cs:11-20

Run Scope Profiles:

Profile Use Case Description
InlineUi Modal commands, RevitTest Executes directly on Revit UI thread
QueuedModeless Long-running/modeless tools Uses ExternalEvent/RevitTask

Source: src/DBTools.Core/Revit/Execution/IRevitRunScope.cs:22-36


ISettingsProvider / IOptionsMonitor

Type-safe settings access with persistence and hot-reload support.

public interface ISettingsProvider
{
    TSettings Get<TSettings>() where TSettings : class, new();
    Task SaveAsync<TSettings>(string section, TSettings settings, 
        CancellationToken ct = default) where TSettings : class;
}

Source: src/DBTools.Core/Settings/ISettingsProvider.cs:1-8

Usage:

// Via ISettingsProvider (singleton)
var settings = context.Resolve<ISettingsProvider>();
var gmSettings = settings.Get<GmSettings>();

// Via IOptionsMonitor (for reactive updates)
var monitor = context.Resolve<IOptionsMonitor<GmSettings>>();
var currentSettings = monitor.CurrentValue;

Source: src/DBTools.App/Bootstrap/DiAppRuntime.cs:78


Shared UI from DBTools.Themes

BrushKeys

All theme brushes are defined using ComponentResourceKey for reliable cross-template resolution:

public static class BrushKeys
{
    // Brand colors
    public static ComponentResourceKey Primary => new(typeof(BrushKeys), "Brush.Primary");
    public static ComponentResourceKey Secondary => new(typeof(BrushKeys), "Brush.Secondary");
    
    // Surface colors
    public static ComponentResourceKey Paper => new(typeof(BrushKeys), "Brush.Paper");
    public static ComponentResourceKey Surface => new(typeof(BrushKeys), "Brush.Surface");
    public static ComponentResourceKey CardSurface => new(typeof(BrushKeys), "Brush.CardSurface");
    
    // Text colors (WCAG AA compliant)
    public static ComponentResourceKey Body => new(typeof(BrushKeys), "Brush.Body");
    public static ComponentResourceKey TextSecondary => new(typeof(BrushKeys), "Brush.TextSecondary");
    public static ComponentResourceKey TextMuted => new(typeof(BrushKeys), "Brush.TextMuted");
    
    // Status colors
    public static ComponentResourceKey Success => new(typeof(BrushKeys), "Brush.Success");
    public static ComponentResourceKey Warning => new(typeof(BrushKeys), "Brush.Warning");
    public static ComponentResourceKey Error => new(typeof(BrushKeys), "Brush.Error");
    
    // DataGrid specific
    public static ComponentResourceKey DataGridRowSelected => new(typeof(BrushKeys), "Brush.DataGrid.RowSelected");
    public static ComponentResourceKey DataGridAccentStripe => new(typeof(BrushKeys), "Brush.DataGrid.AccentStripe");
    
    // ... 130+ additional keys
}

Source: src/DBTools.Themes/BrushKeys.cs:9-139

Usage in XAML:

<Window xmlns:theme="clr-namespace:DBTools.Themes;assembly=DBTools.Themes">
    <Border Background="{DynamicResource {x:Static theme:BrushKeys.Surface}}">
        <TextBlock Text="Hello"
                   Foreground="{DynamicResource {x:Static theme:BrushKeys.Body}}"/>
    </Border>
</Window>

Design Tokens

Standard spacing, sizing, and typography values for consistent layouts:

<!-- Spacing -->
<Thickness x:Key="Spacing4">4</Thickness>
<Thickness x:Key="Spacing8">8</Thickness>
<Thickness x:Key="Spacing16">16</Thickness>

<!-- Padding -->
<Thickness x:Key="Pad8">8</Thickness>
<Thickness x:Key="Card.Padding">16,12</Thickness>

<!-- Corner radius -->
<CornerRadius x:Key="Radius4">4</CornerRadius>
<CornerRadius x:Key="Radius8">8</CornerRadius>

<!-- Typography -->
<sys:Double x:Key="FontSize.Body">13</sys:Double>
<sys:Double x:Key="FontSize.Title">16</sys:Double>
<sys:Double x:Key="FontSize.Header">20</sys:Double>

Source: src/DBTools.Themes/Themes/App.Tokens.xaml:5-72


Window Base Classes

DbtWindowBase

Standard modal window with automatic theme loading and progress overlay:

public class DbtWindowBase : Window, IWindowWithOwnerProvider, IThemeOptOut
{
    public DbtWindowBase()
    {
        DbtWindowInitHelper.Initialize(this, nameof(DbtWindowBase));
    }
    
    public IWindowOwnerProvider? OwnerProvider { get; set; }
    public bool UseDefaultTheme { get; set; }
}

Source: src/DBTools.Core/UI/Windows/DbtWindowBase.cs:27-47

DbtRibbonWindowBase

Ribbon window for tools with tab-based navigation:

public class DbtRibbonWindowBase : RibbonWindow, IWindowWithOwnerProvider, IThemeOptOut
{
    public DbtRibbonWindowBase()
    {
        DbtWindowInitHelper.Initialize(this, nameof(DbtRibbonWindowBase));
    }
}

Source: src/DBTools.Core/UI/Windows/DbtRibbonWindowBase.cs:15-35

What window bases provide:

  • Window-scoped theme loading (avoids cross-add-in conflicts)
  • Revit window ownership binding
  • Progress overlay integration
  • UI error protection
  • DBTools icon

Source: src/DBTools.Core/UI/Windows/DbtWindowInitHelper.cs:44-78


DI Patterns

Creating a Tool Module

Every tool implements DbtToolModule to register its services:

public sealed class GmToolModule : DbtToolModule
{
    public override void RegisterServices(IServiceCollection services, DbtToolManifest manifest)
    {
        Guard.NotNull(services, nameof(services));
        Guard.NotNull(manifest, nameof(manifest));
        services.AddGmServices();  // Extension method for cleaner registration
    }
}

Source: src/Tools/Common/GM/GmToolModule.cs:10-18

DbtToolModule Base Class

public abstract class DbtToolModule
{
    /// <summary>Registers tool settings types (Options pattern).</summary>
    public virtual void RegisterSettings(IServiceCollection services, 
        IConfiguration configuration, DbtToolManifest manifest) { }
    
    /// <summary>Registers services required by this tool module.</summary>
    public virtual void RegisterServices(IServiceCollection services, 
        DbtToolManifest manifest) { }
    
    /// <summary>Registers settings pack definitions for the Settings UI.</summary>
    public virtual void RegisterSettingsPacks(IServiceCollection services, 
        DbtToolManifest manifest) { }
    
    /// <summary>Registers hook handlers with the tool registry.</summary>
    public virtual void RegisterHooks(DbtToolRegistry registry, 
        DbtToolManifest manifest) { }
}

Source: src/DBTools.Core/Tools/DbtToolModule.cs:9-49

Service Registration Patterns

public static class GmServiceCollectionExtensions
{
    public static IServiceCollection AddGm(this IServiceCollection services)
    {
        // === Pure/shared logic (scoped) ===
        services.AddScoped<IProjectLifecycleService, ProjectLifecycleService>();
        services.AddScoped<INamingSimilarityService, NamingSimilarityService>();
        
        // === Services with complex initialization ===
        services.AddScoped<IGmMappingService>(sp =>
        {
            var logger = sp.GetRequiredService<ILogger<GmMappingService>>();
            var usage = sp.GetRequiredService<IUsageIndexService>();
            var tx = sp.GetRequiredService<ITransactionRunner>();
            // ... resolve other dependencies
            return new GmMappingService(logger, usage, tx, writers);
        });
        
        // === Revit infrastructure (scoped) ===
        services.AddScoped<ICategoryService, CategoryService>();
        services.AddScoped<IElementQuery, ElementQueryService>();
        
        // === UI services ===
        services.AddScoped<ICommitReviewWindowService, CommitReviewWindowService>();
        
        return services;
    }
}

Source: src/Tools/Common/GM/Shell/DI/Services.cs:28-108

Resolving Services in Commands

protected override async Task RunAsync(IDbtToolContext context)
{
    // Single service
    var alerts = context.Resolve<IAlertService>();
    
    // Multiple services (tuple pattern)
    var (tx, groups, mapping, planner, lifecycle) = context.Resolve<
        ITransactionRunner,
        ITransactionGroupService,
        IGmMappingService,
        IGmPlanningService,
        IProjectLifecycleService>();
}

Source: src/Tools/Common/GM/Features/GmCommand.cs:59-91


Best Practices

1. Always Use ISafeExecutor at Entrypoints

// DO: Inherit from DbtToolCommand (uses ISafeExecutor internally)
public sealed class MyCommand : DbtToolCommand
{
    protected override async Task RunAsync(IDbtToolContext context)
    {
        // ISafeExecutor wraps this automatically
    }
}

// DON'T: Create IExternalCommand without error handling
public class BadCommand : IExternalCommand
{
    public Result Execute(...) 
    {
        // No error handling - will crash Revit
    }
}

Source: src/DBTools.Core/Revit/Execution/DbtToolCommand.cs:35-126

2. Prefer Scoped Services for Revit-Dependent Code

// DO: Scoped registration for services that need Revit context
services.AddScoped<ICategoryService, CategoryService>();
services.AddScoped<IElementQuery, ElementQueryService>();

// DON'T: Singleton for services that capture Document references
services.AddSingleton<IBadService, ServiceThatHoldsDocument>();  // Memory leak!

3. Use Transaction Services, Not Raw Transactions

// DO: Use ITransactionRunner
await _tx.RunAsync("Create Wall", doc =>
{
    Wall.Create(doc, curve, levelId, false);
});

// DON'T: Create raw Transaction objects
using var tx = new Transaction(doc, "Create Wall");
tx.Start();
Wall.Create(doc, curve, levelId, false);
tx.Commit();  // Manual error handling required

4. Reference Theme Brushes via BrushKeys

<!-- DO: Use BrushKeys for reliable resolution -->
<Border Background="{DynamicResource {x:Static theme:BrushKeys.Surface}}"/>

<!-- DON'T: Use string keys (fragile) -->
<Border Background="{DynamicResource Brush.Surface}"/>

5. Inherit from Window Base Classes

// DO: Inherit from DbtWindowBase
public partial class MyToolWindow : DbtWindowBase
{
    // Theme, progress overlay, error protection all included
}

// DON'T: Use raw Window
public partial class MyToolWindow : Window
{
    // No theme, no error protection, no Revit ownership
}

Anti-Patterns to Avoid

1. Service Locator Pattern

// DON'T: Use AppRuntime.Resolve outside of command constructors
public class BadService
{
    public void DoWork()
    {
        var alerts = AppRuntime.Resolve<IAlertService>();  // Hidden dependency
    }
}

// DO: Inject via constructor
public class GoodService
{
    private readonly IAlertService _alerts;
    
    public GoodService(IAlertService alerts)
    {
        _alerts = alerts;
    }
}

2. Silent Failures

// DON'T: Swallow exceptions
try { DoWork(); }
catch { /* silent failure */ }

// DON'T: Return defaults pretending success
catch (Exception) { return new List<Element>(); }

// DO: Let ISafeExecutor handle errors
await executor.RunAsync(async () =>
{
    DoWork();  // Exception propagates to error handling
}, logger, notifier);

3. Capturing Document References

// DON'T: Hold Document references in singleton services
public class BadService
{
    private readonly Document _doc;  // Will become stale!
    
    public BadService(Document doc) => _doc = doc;
}

// DO: Accept Document per-call
public class GoodService
{
    public void DoWork(Document doc)
    {
        // Fresh reference each call
    }
}

4. Raw Console/MessageBox Output

// DON'T: Use Console or MessageBox
Console.WriteLine("Debug info");
MessageBox.Show("Error occurred");

// DO: Use logging and IAlertService
_logger.LogDebug("Debug info");
_alerts.Show(new AlertRequest("Error", new MessageBodyViewModel("Error occurred")));

5. Bypassing Theme System

// DON'T: Use hard-coded colors
<Border Background="#222228"/>

// DO: Use theme brushes
<Border Background="{DynamicResource {x:Static theme:BrushKeys.Surface}}"/>


Quick Reference Table

Need Service Lifetime
Error handling at entrypoints ISafeExecutor Singleton
Model modifications ITransactionRunner Scoped
Batch undo operations ITransactionGroupService Scoped
User dialogs IAlertService Singleton
Logging ILogger<T> Singleton
Revit context IRevitRunScope Scoped
Thread-safe API access IRevitCallGate Scoped
Settings access ISettingsProvider Singleton
Reactive settings IOptionsMonitor<T> Singleton