Blog / Creating C# Plugins for Revit: A Complete Developer Guide

Creating C# Plugins for Revit: A Complete Developer Guide

Build custom C# plugins for Autodesk Revit. Covers project setup, the .NET 8 move in Revit 2025, external commands, debugging, UI integration and deployment.

A
Archgyan Editor
· Updated · 30 min read

Go deeper with Archgyan Academy

Structured BIM and Revit learning paths for architects and students.

Explore Academy →

Introduction

Autodesk Revit is one of the most widely used BIM tools in the AEC industry, but its out-of-the-box functionality only goes so far. Every firm has unique workflows, naming standards, and automation needs that Revit’s built-in features cannot address. This is where C# plugin development becomes a game-changer.

By writing custom plugins with C# and the Revit API, you can automate repetitive tasks, enforce company standards, extract data for reporting, and build entirely new tools that integrate directly into the Revit interface. Whether you want to batch-rename families, auto-generate sheets, or validate model quality before handoff, the Revit API gives you full programmatic access to the BIM model.

This guide walks you through the entire process of creating a Revit plugin in C# - from setting up your development environment to deploying your finished add-in. You do not need prior Revit API experience, but a basic understanding of C# syntax and object-oriented programming will help you follow along.

One thing has changed since most Revit API tutorials were written, and it is now the biggest single source of “it builds fine but the debugger fails” messages. Revit moved off .NET Framework. Revit 2025 and 2026 run on .NET 8, and Revit 2027 runs on .NET 10. A project still targeting .NET Framework 4.8 will often compile without a single warning and then fall over the moment you press F5. The short video below shows that exact failure and the fix, start to finish, in Visual Studio 2022.

If that is the problem you arrived with, the section “Fixing the Debugger Error When Your Project Targets the Wrong .NET” further down walks through every step in the recording. If you are starting from nothing, read on from here.

Prerequisites and Development Environment Setup

Before writing any code, you need the right tools installed and configured. The Revit API is a .NET library, and your project has to target the same .NET version the Revit you are building for actually runs on. Getting that one line wrong causes most of the first-day frustration.

Required Software

  • Visual Studio 2022 (Community edition is free) with the ”.NET Desktop Development” workload installed
  • Autodesk Revit installed on the same machine, in the version you are targeting. The API assemblies you reference must come from that version
  • The .NET runtime your target Revit uses. This is version-specific, and the table below is the part to get right before you create the project

Which .NET version your target Revit needs

Revit sat on .NET Framework for years at a time, which is why so much published Revit API material assumes it. That stopped with Revit 2025. Pick the framework from the Revit version you are building for, not from habit:

Target Revit versionFramework monikerWhat you select in Visual Studio
Revit 2023, 2024net48.NET Framework 4.8
Revit 2025, 2026net8.0-windows.NET 8
Revit 2027net10.0-windows.NET 10 (requires the .NET 10 SDK)

Two things follow from this table:

  1. Any tutorial that tells you to create a Class Library (.NET Framework) project is correct for Revit 2024 and earlier and wrong for Revit 2025 and later. This guide flags both cases at each step.
  2. If your firm runs more than one Revit version across live projects, you are not shipping one DLL. You are multi-targeting, which the version compatibility section near the end covers.

The jump is also not on a fixed cadence. Revit 2025 and 2026 share .NET 8, then 2027 moves to .NET 10. Confirm the runtime for each new Revit release rather than assuming the previous one carries forward.

Revit API Assemblies

The two core assemblies you will reference in every Revit plugin project are:

AssemblyLocationPurpose
RevitAPI.dllC:\Program Files\Autodesk\Revit 2025\Core API - elements, documents, geometry, parameters
RevitAPIUI.dllC:\Program Files\Autodesk\Revit 2025\UI API - ribbons, dialogs, selection, task dialogs

These DLLs ship with every Revit installation. You reference them in your project but never copy them to your output folder - Revit loads them at runtime.

  • RevitLookup - an open-source Revit add-in that lets you inspect the Revit database interactively. Install it from the RevitLookup GitHub repository. This tool is indispensable for understanding element hierarchies, parameter names, and family structures while developing.
  • Add-In Manager - allows you to load and reload plugins without restarting Revit during development.

Creating Your First Revit Plugin Project

Let’s build a simple plugin that counts all wall elements in the current Revit model and displays the result in a dialog.

Step 1 - Create the Visual Studio Project

  1. Open Visual Studio 2022 and select Create a new project
  2. Choose the project template that matches your target Revit version:
    • Revit 2024 and earlier: Class Library (.NET Framework)
    • Revit 2025 and later: Class Library, the SDK-style .NET template
  3. Set the target framework from the table above: .NET Framework 4.8 for Revit 2024 and earlier, .NET 8 for Revit 2025 and 2026, .NET 10 for Revit 2027
  4. Name the project MyFirstRevitPlugin
  5. Choose a location and click Create

If you already have an older project and you are now targeting Revit 2025 or later, do not recreate it from scratch. Visual Studio retargets it in place, which is the workflow shown in the video and covered in detail further down.

Step 2 - Add Revit API References

  1. In Solution Explorer, right-click References and select Add Reference
  2. Click Browse and navigate to C:\Program Files\Autodesk\Revit 2025\
  3. Select both RevitAPI.dll and RevitAPIUI.dll
  4. Click Add, then OK
  5. For each reference, set Copy Local to False in the Properties panel. This is critical - if you copy these DLLs to your output folder, Revit may load conflicting versions and crash

Step 3 - Write the External Command

Replace the default class file contents with:

using Autodesk.Revit.Attributes;
using Autodesk.Revit.DB;
using Autodesk.Revit.UI;

namespace MyFirstRevitPlugin
{
    [Transaction(TransactionMode.ReadOnly)]
    public class WallCounter : IExternalCommand
    {
        public Result Execute(
            ExternalCommandData commandData,
            ref string message,
            ElementSet elements)
        {
            // Get the active document
            Document doc = commandData.Application
                .ActiveUIDocument.Document;

            // Collect all wall instances in the model
            FilteredElementCollector collector =
                new FilteredElementCollector(doc);
            IList<Element> walls = collector
                .OfClass(typeof(Wall))
                .WhereElementIsNotElementType()
                .ToElements();

            // Display the count
            TaskDialog.Show("Wall Counter",
                $"This model contains {walls.Count} wall instances.");

            return Result.Succeeded;
        }
    }
}

Key points about this code:

  • IExternalCommand is the interface every Revit command plugin must implement. It requires a single Execute method.
  • [Transaction(TransactionMode.ReadOnly)] tells Revit this command only reads the model - it will not modify anything. Use TransactionMode.Manual when you need to make changes.
  • FilteredElementCollector is the primary way to query elements from the Revit database. It is fast and memory-efficient because it filters at the database level rather than loading all elements into memory.
  • TaskDialog is Revit’s built-in dialog class. For simple messages, it works like MessageBox but follows Revit’s UI conventions.

Step 4 - Build the Project

Press Ctrl+Shift+B to build. The output DLL will be in bin\Debug\MyFirstRevitPlugin.dll.

The Add-In Manifest File

Revit discovers plugins through .addin manifest files placed in specific folders. Without this file, Revit does not know your plugin exists.

Create a file named MyFirstRevitPlugin.addin with the following content:

<?xml version="1.0" encoding="utf-8"?>
<RevitAddIns>
  <AddIn Type="Command">
    <Name>Wall Counter</Name>
    <Assembly>C:\RevitPlugins\MyFirstRevitPlugin.dll</Assembly>
    <FullClassName>MyFirstRevitPlugin.WallCounter</FullClassName>
    <AddInId>A1B2C3D4-E5F6-7890-ABCD-EF1234567890</AddInId>
    <VendorId>YourCompany</VendorId>
    <VendorDescription>Your Company Name</VendorDescription>
  </AddIn>
</RevitAddIns>

Important fields:

  • Assembly - the full path to your compiled DLL. Update this to match your actual output location.
  • FullClassName - the namespace-qualified class name that implements IExternalCommand.
  • AddInId - a unique GUID. Generate one in Visual Studio using Tools > Create GUID or use Guid.NewGuid() in C#. Every command needs its own unique GUID.

Where to Place the Manifest

Copy the .addin file to one of these locations:

LocationScope
C:\ProgramData\Autodesk\Revit\Addins\2025\All users on this machine
%AppData%\Autodesk\Revit\Addins\2025\Current user only

The folder name (2025) must match your Revit version. After placing the file, restart Revit. Your command will appear under the Add-Ins tab in the External Tools dropdown.

Understanding Revit Transactions

Any operation that modifies the Revit model must be wrapped in a Transaction. This is one of the most common sources of errors for beginners - attempting to change an element without an open transaction will throw an InvalidOperationException.

Basic Transaction Pattern

[Transaction(TransactionMode.Manual)]
public class RenameWalls : IExternalCommand
{
    public Result Execute(
        ExternalCommandData commandData,
        ref string message,
        ElementSet elements)
    {
        Document doc = commandData.Application
            .ActiveUIDocument.Document;

        FilteredElementCollector collector =
            new FilteredElementCollector(doc);
        IList<Element> walls = collector
            .OfClass(typeof(Wall))
            .WhereElementIsNotElementType()
            .ToElements();

        using (Transaction tx = new Transaction(doc, "Rename Walls"))
        {
            tx.Start();

            foreach (Element wall in walls)
            {
                Parameter commentParam = wall.LookupParameter("Comments");
                if (commentParam != null && !commentParam.IsReadOnly)
                {
                    commentParam.Set("Reviewed");
                }
            }

            tx.Commit();
        }

        TaskDialog.Show("Done",
            $"Updated comments on {walls.Count} walls.");
        return Result.Succeeded;
    }
}

Transaction Rules to Remember

  1. Always use TransactionMode.Manual when your command modifies the model
  2. Call tx.Start() before any modifications and tx.Commit() after
  3. Use a using block so the transaction is disposed even if an exception occurs
  4. Give each transaction a descriptive name - this becomes the undo label in Revit’s Edit menu
  5. Never nest transactions - use SubTransaction or TransactionGroup for complex multi-step operations
  6. If something goes wrong, call tx.RollBack() instead of tx.Commit() to undo all changes within that transaction

Error Handling Pattern

A production-quality command should wrap the transaction in try-catch:

using (Transaction tx = new Transaction(doc, "My Operation"))
{
    tx.Start();
    try
    {
        // Modify elements here
        tx.Commit();
    }
    catch (Exception ex)
    {
        tx.RollBack();
        message = ex.Message;
        return Result.Failed;
    }
}

Working with the FilteredElementCollector

The FilteredElementCollector is the workhorse of the Revit API. Understanding how to use it efficiently is essential for plugin performance.

Common Filter Patterns

// All doors in the model
var doors = new FilteredElementCollector(doc)
    .OfCategory(BuiltInCategory.OST_Doors)
    .WhereElementIsNotElementType()
    .ToElements();

// All floor types (family types, not instances)
var floorTypes = new FilteredElementCollector(doc)
    .OfClass(typeof(FloorType))
    .ToElements();

// All elements in a specific view
var viewElements = new FilteredElementCollector(doc, viewId)
    .WhereElementIsNotElementType()
    .ToElements();

// Rooms with a specific parameter value
var largeRooms = new FilteredElementCollector(doc)
    .OfCategory(BuiltInCategory.OST_Rooms)
    .WhereElementIsNotElementType()
    .Cast<Room>()
    .Where(r => r.Area > 50.0)
    .ToList();

Performance Best Practices

  1. Apply quick filters first. OfClass() and OfCategory() are “quick filters” that run at the database level before elements are loaded into memory. Always use these before slower LINQ queries.
  2. Avoid .ToElements() when you only need a count. Use .GetElementCount() instead.
  3. Scope to a view when possible. Passing a viewId to the collector limits the search to elements visible in that view, which is significantly faster for large models.
  4. Combine filters with IntersectionFilter instead of running multiple collectors:
var categoryFilter =
    new ElementCategoryFilter(BuiltInCategory.OST_Walls);
var classFilter =
    new ElementClassFilter(typeof(FamilyInstance));
var combinedFilter =
    new LogicalAndFilter(categoryFilter, classFilter);

var results = new FilteredElementCollector(doc)
    .WherePasses(combinedFilter)
    .ToElements();

Adding a Ribbon Button and Custom UI

The External Tools dropdown works for testing, but a proper plugin should have its own ribbon tab with buttons. This requires implementing IExternalApplication instead of (or alongside) IExternalCommand.

Creating a Ribbon Tab

public class MyApp : IExternalApplication
{
    public Result OnStartup(UIControlledApplication app)
    {
        // Create a custom ribbon tab
        string tabName = "My Tools";
        app.CreateRibbonTab(tabName);

        // Create a panel within the tab
        RibbonPanel panel = app.CreateRibbonPanel(tabName, "Utilities");

        // Get the path to this assembly
        string assemblyPath =
            Assembly.GetExecutingAssembly().Location;

        // Create a button for the WallCounter command
        PushButtonData buttonData = new PushButtonData(
            "WallCounter",           // internal name
            "Count\nWalls",          // button label (use \n for two lines)
            assemblyPath,
            "MyFirstRevitPlugin.WallCounter"  // full class name
        );

        // Set button icon (32x32 for large, 16x16 for small)
        buttonData.LargeImage = new BitmapImage(
            new Uri("pack://application:,,,/MyFirstRevitPlugin;component/Resources/icon32.png")
        );
        buttonData.ToolTip = "Counts all wall instances in the current model";

        PushButton button = panel.AddItem(buttonData) as PushButton;

        return Result.Succeeded;
    }

    public Result OnShutdown(UIControlledApplication app)
    {
        return Result.Succeeded;
    }
}

Updating the Manifest for an Application

When you use IExternalApplication, the .addin manifest changes slightly:

<RevitAddIns>
  <AddIn Type="Application">
    <Name>My Tools</Name>
    <Assembly>C:\RevitPlugins\MyFirstRevitPlugin.dll</Assembly>
    <FullClassName>MyFirstRevitPlugin.MyApp</FullClassName>
    <AddInId>B2C3D4E5-F6A7-8901-BCDE-F12345678901</AddInId>
    <VendorId>YourCompany</VendorId>
    <VendorDescription>Your Company Name</VendorDescription>
  </AddIn>
</RevitAddIns>

Note that the Type is now Application instead of Command. You can include both Application and Command entries in the same .addin file.

Adding Icons to Your Buttons

  1. Add a Resources folder to your Visual Studio project
  2. Add 32x32 and 16x16 PNG images for your button icons
  3. Set each image’s Build Action to Resource in the Properties panel
  4. Reference them using the pack:// URI scheme as shown above

A button without an icon still works but looks unprofessional. Aim for simple, recognizable icons that communicate the command’s purpose at a glance.

Reading and Writing Parameters

Parameters are how data is stored on Revit elements. Understanding how to read and write them is fundamental to most plugins.

Built-In Parameters vs Shared Parameters

// Reading a built-in parameter by BuiltInParameter enum
Parameter levelParam = wall.get_Parameter(
    BuiltInParameter.WALL_BASE_CONSTRAINT);
string levelName = levelParam.AsValueString();

// Reading a shared/project parameter by name
Parameter customParam = wall.LookupParameter("Fire Rating");
if (customParam != null)
{
    string value = customParam.AsString();
}

// Writing a parameter value (must be inside a Transaction)
Parameter comments = wall.LookupParameter("Comments");
if (comments != null && !comments.IsReadOnly)
{
    comments.Set("Checked by plugin");
}

Parameter Types and Value Access

Parameter Storage TypeRead MethodWrite Method
StringAsString()Set(string)
IntegerAsInteger()Set(int)
DoubleAsDouble()Set(double)
ElementIdAsElementId()Set(ElementId)

Important: AsDouble() returns values in Revit’s internal units (feet for length, radians for angles). Use UnitUtils.ConvertFromInternalUnits() to convert to display units.

double lengthFeet = wall.get_Parameter(
    BuiltInParameter.CURVE_ELEM_LENGTH).AsDouble();
double lengthMeters = UnitUtils.ConvertFromInternalUnits(
    lengthFeet, UnitTypeId.Meters);

Creating a Practical Plugin - Model Quality Checker

Let’s combine everything into a useful real-world plugin that checks a model for common quality issues.

[Transaction(TransactionMode.ReadOnly)]
public class ModelChecker : IExternalCommand
{
    public Result Execute(
        ExternalCommandData commandData,
        ref string message,
        ElementSet elements)
    {
        Document doc = commandData.Application
            .ActiveUIDocument.Document;

        var issues = new List<string>();

        // Check 1: Walls with zero length
        var zeroWalls = new FilteredElementCollector(doc)
            .OfClass(typeof(Wall))
            .WhereElementIsNotElementType()
            .Cast<Wall>()
            .Where(w =>
            {
                Parameter len = w.get_Parameter(
                    BuiltInParameter.CURVE_ELEM_LENGTH);
                return len != null && len.AsDouble() < 0.01;
            })
            .ToList();

        if (zeroWalls.Count > 0)
            issues.Add(
                $"Found {zeroWalls.Count} walls with near-zero length");

        // Check 2: Rooms without names
        var unnamedRooms = new FilteredElementCollector(doc)
            .OfCategory(BuiltInCategory.OST_Rooms)
            .WhereElementIsNotElementType()
            .Cast<Room>()
            .Where(r =>
                string.IsNullOrWhiteSpace(
                    r.get_Parameter(BuiltInParameter.ROOM_NAME)
                        ?.AsString()))
            .ToList();

        if (unnamedRooms.Count > 0)
            issues.Add(
                $"Found {unnamedRooms.Count} rooms without names");

        // Check 3: Unplaced rooms
        var unplacedRooms = new FilteredElementCollector(doc)
            .OfCategory(BuiltInCategory.OST_Rooms)
            .WhereElementIsNotElementType()
            .Cast<Room>()
            .Where(r => r.Area == 0)
            .ToList();

        if (unplacedRooms.Count > 0)
            issues.Add(
                $"Found {unplacedRooms.Count} unplaced rooms (zero area)");

        // Check 4: Warnings count
        IList<FailureMessage> warnings = doc.GetWarnings();
        if (warnings.Count > 0)
            issues.Add(
                $"Model has {warnings.Count} active warnings");

        // Display results
        string report = issues.Count > 0
            ? string.Join("\n", issues)
            : "No issues found. Model looks clean.";

        TaskDialog td = new TaskDialog("Model Quality Report");
        td.MainInstruction = issues.Count > 0
            ? $"Found {issues.Count} issue(s)"
            : "All checks passed";
        td.MainContent = report;
        td.MainIcon = issues.Count > 0
            ? TaskDialogIcon.TaskDialogIconWarning
            : TaskDialogIcon.TaskDialogIconNone;
        td.Show();

        return Result.Succeeded;
    }
}

This kind of quality checker is something every BIM team needs. You can extend it with checks for naming conventions, parameter completeness, correct level assignments, and model extents.

Fixing the Debugger Error When Your Project Targets the Wrong .NET

This is the failure that catches nearly everyone moving to Revit 2025 or later, and it is worth walking through slowly because the symptom points away from the cause.

The symptom: the build passes, the debug run does not

In the video the solution builds cleanly. Watch the build succeed at 0:10. The debug run is then started and it stops with an error. The failure appears at 0:19. Nothing in the build output warned about it, which is exactly why people lose an afternoon to the .addin manifest or to Copy Local settings before they think to look at the framework.

The cause is a runtime mismatch. Revit 2025 and later host add-ins on modern .NET, so a class library still compiled for .NET Framework 4.8 cannot load into the process the debugger is launching. The reason is given at 0:24: the project has to move off .NET Framework onto the .NET line Revit now uses.

A useful mental model: the compiler only checks that your code is valid against the references you gave it. It has no opinion about which runtime will eventually host the DLL. That check happens at load time, inside Revit, long after the build reported success.

Step 1 - Install the .NET upgrade tooling

Close Visual Studio and reopen it with Continue without code, then go to Extensions > Manage Extensions and search for the .NET upgrade extension. The extension search is at 0:46. Install it.

Visual Studio cannot patch itself while it is running, so the installer waits for you to close the IDE and then asks you to confirm. The installer prompt and the Modify step are at 1:08. Let it finish completely before reopening. Interrupting a VSIX install halfway leaves the extension registered but not functional, and the upgrade command then simply will not appear on the context menu.

Step 2 - The extension on its own fixes nothing

This is the beat most write-ups compress away, and it is the one worth keeping. Reopen Visual Studio, reopen the project, and the error is still there. Confirmed at 1:25.

The extension is tooling, not a fix. It adds an upgrade command to the IDE. You still have to run that command against your project. This matters because it is precisely the moment people conclude the extension was the wrong answer and go back to hunting the manifest, which is where the afternoon disappears.

Step 3 - Run an in-place project upgrade

Right-click the project in Solution Explorer and choose Upgrade. The upgrade command is at 1:28. Then:

  1. Select In-place project upgrade. Shown at 1:33. In-place retargets the project you already have rather than generating a side-by-side copy, so your references, code files, resources and manifest all stay where they are. The side-by-side option exists for cases where you need the old project to keep building; for a Revit add-in you are usually moving forward for good.
  2. Choose the target framework. In the video that is .NET 8, which is correct for Revit 2025 and 2026. The framework choice is at 1:37. If you are targeting Revit 2027, choose .NET 10 instead. Do not just take the newest entry in the list. A project built for a newer runtime than Revit hosts fails in the same way, only from the opposite direction, and the error gives you no hint which side is wrong.
  3. Click Upgrade selection. Shown at 1:42. The tool rewrites the project file and swaps out what it needs to.

Commit before you do this. The upgrade rewrites the .csproj in place, and having a clean diff afterwards is the fastest way to see what it actually changed.

Step 4 - Verify the target framework before debugging again

Open the project properties and read the target framework back. The verification step is at 1:50. It should now report .NET 8, or whichever version you selected.

This check takes five seconds and it is worth the habit. It tells you whether the upgrade took, which is far more useful than pressing F5 and then trying to interpret a second failure that might have a completely different cause.

Step 5 - Debug, then let Revit load the add-in

Start the debug run again. The successful run begins at 1:57. Revit launches under the debugger. Revit comes up at 2:02.

Revit then asks whether to load an add-in it does not recognise. This dialog surprises people the first time, because a normal Revit session never shows it. Choose Load Once. The prompt and the Load Once choice are at 2:09.

ChoiceEffectWhen to use it
Always LoadRevit stops asking about this add-inOnce the tool is stable and deployed to the team
Load OnceLoads for this session onlyThe right default while developing
Do Not LoadSkips the add-in entirelyWhen you suspect your own code is what is crashing Revit at startup

Keeping the prompt during development is genuinely useful. It confirms Revit found your manifest and your DLL on every run, which quietly rules out half the deployment problems before you have even reached your breakpoint.

With the session running, the debugger is attached and the add-in is live. The attached debugger is visible at 2:16.

Step 6 - Confirm the command is actually there

Start a project in the running Revit instance and open the Add-Ins tab. The command appears under Add-Ins at 2:25. Commands registered from an .addin manifest with Type="Command" land under External Tools rather than on their own ribbon tab, which is why the walkthrough finds it there. Getting your own tab requires IExternalApplication, covered in the ribbon section above.

In the video the command is run against a detail line to return its element ID. The command runs at 2:41. A trivial tool, but the right kind of first tool: it proves the whole chain works, from build through manifest through load through execution, with almost no code that can be wrong.

If the command does not appear at all, the framework was never your problem. Go back to the manifest. Wrong FullClassName, a stale Assembly path, or the .addin file sitting in the folder for a different Revit year are the usual three causes, in that order.

What Else Changes When You Move a Plugin to .NET 8

The fix above gets a sample project building and debugging again. On a real add-in with a few years of history, retargeting usually surfaces a second wave of work that a three-minute demonstration has no room for. None of the following is shown in the video. It is what to expect when you run the same upgrade on something larger.

The project file becomes SDK-style

The upgrade converts the old verbose .csproj into the modern SDK-style format. That is a net win: a much shorter file, no more per-file <Compile Include> entries, and PackageReference instead of packages.config. It also breaks a few habits.

File globbing is on by default, so anything sitting in the project folder gets compiled whether you meant it to or not. Old backup copies of a class file, saved as Command.cs.bak, are fine; a stray Command_old.cs is a duplicate type error. Desktop UI frameworks now need explicit opt-in:

<PropertyGroup>
  <TargetFramework>net8.0-windows</TargetFramework>
  <UseWPF>true</UseWPF>
  <UseWindowsForms>true</UseWindowsForms>
</PropertyGroup>

Without UseWindowsForms, common types like FolderBrowserDialog simply do not resolve, and the error message points at a missing namespace rather than at a missing property.

Some Revit API members changed in the same release

The runtime move landed alongside API changes, so retargeting and fixing compile errors tend to arrive in the same afternoon. The one that hits nearly every existing add-in is ElementId. The old IntegerValue property is gone in Revit 2025 and later, replaced by Value, which returns a long rather than an int. Any code that stored element IDs in an int has to widen.

If you support both old and new Revit versions from one codebase, guard it:

#if REVIT2025 || REVIT2026 || REVIT2027
    long idValue = element.Id.Value;
#else
    long idValue = element.Id.IntegerValue;
#endif

Every new Revit year has to be added to that guard by hand. Forgetting is a quiet failure rather than a loud one: the build for the new year compiles the old branch and you find out at runtime.

Multi-targeting instead of a single DLL

Once your firm runs two Revit versions across live projects, one build no longer covers you. A single project can produce a DLL per Revit version by switching the target framework on a build property:

<PropertyGroup Condition="'$(RevitVersion)' == '2024'">
  <TargetFramework>net48</TargetFramework>
  <DefineConstants>REVIT2024</DefineConstants>
</PropertyGroup>

<PropertyGroup Condition="'$(RevitVersion)' == '2025' Or '$(RevitVersion)' == '2026'">
  <TargetFramework>net8.0-windows</TargetFramework>
  <DefineConstants>REVIT2025</DefineConstants>
</PropertyGroup>

Build each version explicitly, then deploy the matching DLL alongside an .addin file in that year’s folder. When you add a new Revit year you have to touch all three places at once: the framework property group, the API package version, and every #if guard in the codebase. Miss the guards and the new year builds green while silently running the old code path.

Tool migration and model migration usually land in the same quarter, and it helps to plan them as one exercise rather than two. Our guide to planning a Revit version upgrade covers the model and template side of the same move.

Debugging Your Plugin

Debugging a Revit plugin means getting the Visual Studio debugger into the Revit process. There are two ways to do it, and it is worth knowing both because they fail differently.

Option A - Attach to a running Revit

  1. Build your project in Debug configuration
  2. Start Revit normally (not from Visual Studio)
  3. In Visual Studio, go to Debug > Attach to Process
  4. Find Revit.exe in the list and click Attach
  5. Set breakpoints in your code
  6. Run your command from Revit and Visual Studio will break at your breakpoints

Use this when Revit is already open with a model loaded, or when you need to debug something that only reproduces after a long session.

Option B - Launch Revit from Visual Studio with F5

This is the flow shown in the video, and it is the better default for day-to-day work: one keystroke builds, launches Revit and attaches the debugger. The F5 run and Revit launching under the debugger are at 1:57.

The trade-off is where the failures surface. With F5, a runtime mismatch shows up as the debug session refusing to start, which is what makes the problem above so confusing: the build reported success moments earlier, and nothing on screen names the framework as the culprit.

Automating the Debug Workflow

Add a post-build event in your project properties to automatically copy the DLL to the add-ins folder:

xcopy /Y "$(TargetDir)$(TargetFileName)" "C:\RevitPlugins\"

Then set the project’s Debug settings:

  • Start external program: C:\Program Files\Autodesk\Revit 2025\Revit.exe
  • This lets you press F5 in Visual Studio to launch Revit with the debugger already attached.

Point that path at the same Revit version your project targets. Mixing them, for example a project on net8.0-windows launching a Revit 2024 executable, produces exactly the load failure described above with none of the clues.

When Revit starts this way it will ask whether to load your add-in. Choose Load Once while developing. The prompt is shown at 2:09.

Common Debugging Pitfalls

  • The build succeeds but the debug run fails immediately. Check the target framework first, before anything else. This is the Revit 2025 and later runtime mismatch, and the walkthrough above fixes it in about two minutes.
  • Revit locks the DLL while running. You must close Revit to rebuild. Use an Add-In Manager tool to hot-reload during development.
  • Missing references at runtime often mean you forgot to set Copy Local to False for the Revit API DLLs.
  • Revit never asks whether to load your add-in. That prompt is your confirmation that Revit found the manifest and the DLL. No prompt means Revit is not seeing the .addin file at all, so check the year folder and the Assembly path rather than your code.
  • Transaction errors almost always mean you tried to modify the model outside of a transaction, or you forgot to call Start().
  • NullReferenceException on parameters - always check if LookupParameter() returns null before accessing its value.
  • Breakpoints show as hollow circles with a “no symbols loaded” tooltip. The DLL Revit loaded is not the one you just built. Usually the post-build copy failed, or the manifest points at an old output folder.

Deployment and Distribution

When your plugin is ready for your team or clients, you need a clean deployment strategy.

Manual Deployment

  1. Create a folder for your plugin files (e.g., C:\RevitPlugins\MyTools\)
  2. Copy your DLL and any dependency DLLs into this folder
  3. Copy the .addin file to the appropriate Addins folder
  4. Make sure the Assembly path in the .addin file points to the correct DLL location

Creating an Installer

For wider distribution, build a Windows Installer (MSI) or use a tool like Inno Setup:

  • Copy the DLL to a standard location (%ProgramFiles%\YourCompany\PluginName\)
  • Place the .addin file in %ProgramData%\Autodesk\Revit\Addins\2025\
  • Handle multiple Revit versions by placing .addin files in each version’s folder
  • Include an uninstaller that removes both the DLL and .addin file

Version Compatibility

Revit plugins compiled against one version’s API may not work with other versions, and since Revit 2025 that is no longer only an API question. It is a runtime question too. A single DLL cannot span the .NET Framework and .NET 8 boundary, so “build once for the oldest version” stops working the moment your supported range crosses Revit 2024 to 2025.

Best practices:

  • Group your supported versions by runtime first. Revit 2023 and 2024 share net48, Revit 2025 and 2026 share net8.0-windows, and Revit 2027 needs net10.0-windows. Each group is a separate build output at minimum.
  • Within a runtime group, target the oldest version you support. Forward compatibility across a shared API generation is generally reliable; across a runtime boundary it does not exist.
  • Use conditional compilation (#if REVIT2025) for version-specific API calls, and grep the whole solution for those symbols whenever you add a Revit year so no guard is left behind.
  • Deploy a matching .addin file into each year’s folder, each pointing at the DLL built for that year.
  • Test on every target version before releasing. A version that builds is not a version that loads.

Common Mistakes and How to Avoid Them

Even experienced developers run into these issues when starting with the Revit API.

  1. Not setting Copy Local to False on Revit API references. This causes assembly loading conflicts and crashes at startup.

  2. Forgetting the Transaction attribute. Every IExternalCommand class must have [Transaction(TransactionMode.Manual)] or [Transaction(TransactionMode.ReadOnly)].

  3. Using LINQ .Count() instead of .GetElementCount(). The LINQ method loads all elements into memory first. The collector method counts at the database level.

  4. Hardcoding file paths. Use Environment.GetFolderPath() and relative paths so your plugin works on different machines.

  5. Not handling the “no document open” case. Always check if commandData.Application.ActiveUIDocument is null before accessing the Document.

  6. Ignoring internal units. All measurements in the Revit API use feet (lengths) and radians (angles). Always convert for display.

  7. Running long operations on the main thread without a progress bar. Use IExternalEventHandler and ExternalEvent for asynchronous operations, and show a ProgressBar for operations that take more than a few seconds.

  8. Not disposing of FilteredElementCollector. While the garbage collector will eventually clean it up, disposing it promptly releases database resources.

  9. Targeting .NET Framework for Revit 2025 or later. The project builds and then refuses to load. This is the most common single cause of “my plugin stopped working after we upgraded Revit”, and it has nothing to do with your code.

  10. Assuming the newest .NET in the dropdown is correct. Match the runtime to the Revit version you are hosting in, not to what your machine has installed.

  11. Still using ElementId.IntegerValue on Revit 2025 or later. It was removed in favour of Value, which returns a long. Widen the variables that hold it rather than casting back to int.

Next Steps and Resources

Once you are comfortable with the basics covered in this guide, here are areas to explore next:

  • Modeless dialogs with WPF - build full-featured UI panels that stay open while users work in Revit, using IExternalEventHandler for thread-safe model access
  • Updaters and events - react to model changes in real-time using IUpdater or document events like DocumentChanged
  • Revit API documentation - the official Revit API Docs site provides searchable class references for all API versions
  • Jeremy Tammik’s blog - The Building Coder is the most comprehensive resource for Revit API development, with over 2,000 posts covering every aspect of the API
  • Dynamo integration - you can call Revit API methods from Dynamo’s Python nodes, or create custom Dynamo nodes in C# that wrap your plugin logic
  • Python as a lighter alternative - not every automation needs a compiled add-in. Our guide to Python scripting for architects across the Rhino and Revit APIs covers when a script beats a plugin
  • What is already on the market - before building, check whether the tool exists. Our roundup of Revit plugins worth installing is a reasonable starting shortlist
  • Turning a plugin into a QA process - a checker is only useful if someone runs it at the right moment. Our BIM model quality assurance workflow covers where automated checks fit in delivery

If you are looking to deepen your Revit skills alongside programming, the Archgyan Academy offers structured courses on Revit workflows that complement your development knowledge with practical modeling expertise.

Conclusion

Building C# plugins for Revit opens up a dimension of BIM work that goes beyond manual modeling. With the patterns covered in this guide - external commands, transactions, filtered element collectors, ribbon UI, parameter access, and deployment - you have the foundation to automate most Revit workflows your firm runs.

Get the environment right before you write a line of code. Match the target framework to the Revit version, verify it in the project properties, and confirm Revit prompts you to load the add-in. Those three checks take under a minute and they remove the class of failure that has nothing to do with your logic.

Start small with a simple utility that solves a real problem in your daily work. A wall counter or parameter checker might seem basic, but it teaches you the core API patterns that scale to complex tools. As you build more plugins, you will develop an intuition for how Revit’s database is structured and how to manipulate it efficiently.

The Revit API surface is large, but the patterns are consistent. Once you understand how transactions, collectors, and parameters work, you can tackle almost any automation challenge your firm needs.

Level up your skills

Ready to learn hands-on?

  • Project-based Revit & BIM courses for architects
  • Go from beginner to confident professional
  • Video lessons you can follow at your own pace
Explore Archgyan Academy
← Back to Blog