Revit API — Transaction Handler Pool — Digitteck
Revit API — Transaction Handler Pool
revit-api-dot-net·3 June 2020·5 min read

Revit API — Transaction Handler Pool

Before reading: for a basic understanding of external events, see the previous article on Revit External Events.

Transaction handling in Revit is not complex — but it's tedious when you need a separate IExternalEventHandler for every individual transaction. There is no reason to define one per operation when a generic worker can accept an Action<Document> and handle any transaction on demand.

This solution uses two approaches in parallel — choose either or combine them:

  1. TransactionUnits folder — an experiment where raising an event creates a new transaction with a custom action inside (registering the action in the Revit context).
  2. TransactionHandling folder — the worker pool described in this article.
Visual Studio solution folder structure for the transaction pool

TransactionManager

The manager owns the worker pool and a concurrent queue of pending TransactionUnits.ExecuteInternal is called both when an item is enqueued and when a worker finishes — so if no worker was available on enqueue, the next release will pick it up automatically:

csharp
public class TransactionManager
{
    private readonly ExternalEventWorkerPool _workerPool;
    private readonly ConcurrentQueue<TransactionUnit> _queue = new();

    public TransactionManager(int workerCount = 4)
    {
        _workerPool = new ExternalEventWorkerPool(workerCount, ReleaseUnit);
    }

    public void Enqueue(TransactionUnit unit)
    {
        _queue.Enqueue(unit);
        ExecuteInternal();
    }

    // Called when a queue item is added AND when a worker finishes
    private void ReleaseUnit(TransactionUnit unit) => ExecuteInternal();

    private void ExecuteInternal()
    {
        if (_queue.IsEmpty) return;

        var worker = _workerPool.TryGetWorker();
        if (worker == null) return; // no worker available — it will retry on next release

        if (_queue.TryDequeue(out var unit))
            worker.Setup(unit);
    }
}

// TransactionUnit — descriptor that carries the action to execute inside a transaction
public class TransactionUnit
{
    public string Name { get; }
    public Action<Document> Action { get; }

    public TransactionUnit(string name, Action<Document> action)
    {
        Name   = name;
        Action = action;
    }
}

ExternalEventWorkerPool

The pool maintains two lists — available and in-use. When a worker is requested it moves to the in-use list; when the transaction completes it moves back. The ReleaseUnit action (passed to each worker) is also triggered on completion, which re-invokes ExecuteInternal to drain the queue:

csharp
public class ExternalEventWorkerPool
{
    private readonly List<ExternalEventWorker> _available = new();
    private readonly List<ExternalEventWorker> _inUse    = new();
    private readonly object _lock = new();

    public ExternalEventWorkerPool(int count, Action<TransactionUnit> releaseUnit)
    {
        for (int i = 0; i < count; i++)
            _available.Add(new ExternalEventWorker(releaseUnit, OnWorkerReleased));
    }

    public ExternalEventWorker TryGetWorker()
    {
        lock (_lock)
        {
            if (!_available.Any()) return null;
            var worker = _available[0];
            _available.RemoveAt(0);
            _inUse.Add(worker);
            return worker;
        }
    }

    // Called by the worker after its transaction completes
    private void OnWorkerReleased(ExternalEventWorker worker)
    {
        lock (_lock)
        {
            _inUse.Remove(worker);
            _available.Add(worker);
        }
    }
}

ExternalEventWorker

Each worker owns one ExternalEventHandler and one ExternalEvent.Setup stores the unit and raises the Revit event, which Revit will execute on its own thread:

csharp
public class ExternalEventWorker
{
    private readonly ExternalEventHandler _handler;
    private readonly ExternalEvent _externalEvent;
    private readonly Action<TransactionUnit> _releaseUnit;
    private readonly Action<ExternalEventWorker> _workerRelease;

    public ExternalEventWorker(
        Action<TransactionUnit> releaseUnit,
        Action<ExternalEventWorker> workerRelease)
    {
        _releaseUnit   = releaseUnit;
        _workerRelease = workerRelease;
        _handler       = new ExternalEventHandler();
        _externalEvent = ExternalEvent.Create(_handler);
    }

    // Called by TransactionManager.ExecuteInternal — sets up the action then raises the Revit event
    public void Setup(TransactionUnit unit)
    {
        _handler.Setup(unit, OnCompleted);
        _externalEvent.Raise();
    }

    private void OnCompleted(TransactionUnit unit)
    {
        _releaseUnit(unit);      // triggers ExecuteInternal to dequeue the next item
        _workerRelease(this);    // returns this worker to the available pool
    }
}

ExternalEventHandler

This is the class Revit actually calls. One handler is assigned per worker, and itsTransactionUnit is set by the TransactionManager → ExternalEventWorker.Setup chain before the event is raised:

csharp
// IExternalEventHandler runs in Revit's context — safe to open transactions here
public class ExternalEventHandler : IExternalEventHandler
{
    private TransactionUnit _unit;
    private Action<TransactionUnit> _onCompleted;

    public void Setup(TransactionUnit unit, Action<TransactionUnit> onCompleted)
    {
        _unit        = unit;
        _onCompleted = onCompleted;
    }

    public void Execute(UIApplication app)
    {
        var doc = app.ActiveUIDocument.Document;

        using var transaction = new Transaction(doc, _unit.Name);
        transaction.Start();
        _unit.Action(doc);
        transaction.Commit();

        _onCompleted(_unit);
    }

    public string GetName() => nameof(ExternalEventHandler);
}

Tags

Revit APIC#.NETExternal Events
digitteck

© 2026 Digitteck