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.
Go deeper with Archgyan Academy
Structured BIM and Revit learning paths for architects and students.
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 version | Framework moniker | What you select in Visual Studio |
|---|---|---|
| Revit 2023, 2024 | net48 | .NET Framework 4.8 |
| Revit 2025, 2026 | net8.0-windows | .NET 8 |
| Revit 2027 | net10.0-windows | .NET 10 (requires the .NET 10 SDK) |
Two things follow from this table:
- 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.
- 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:
| Assembly | Location | Purpose |
|---|---|---|
RevitAPI.dll | C:\Program Files\Autodesk\Revit 2025\ | Core API - elements, documents, geometry, parameters |
RevitAPIUI.dll | C:\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.
Optional But Recommended
- 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
- Open Visual Studio 2022 and select Create a new project
- 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
- 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
- Name the project
MyFirstRevitPlugin - 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
- In Solution Explorer, right-click References and select Add Reference
- Click Browse and navigate to
C:\Program Files\Autodesk\Revit 2025\ - Select both
RevitAPI.dllandRevitAPIUI.dll - Click Add, then OK
- For each reference, set Copy Local to
Falsein 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:
IExternalCommandis the interface every Revit command plugin must implement. It requires a singleExecutemethod.[Transaction(TransactionMode.ReadOnly)]tells Revit this command only reads the model - it will not modify anything. UseTransactionMode.Manualwhen you need to make changes.FilteredElementCollectoris 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.TaskDialogis Revit’s built-in dialog class. For simple messages, it works likeMessageBoxbut 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 implementsIExternalCommand.AddInId- a unique GUID. Generate one in Visual Studio using Tools > Create GUID or useGuid.NewGuid()in C#. Every command needs its own unique GUID.
Where to Place the Manifest
Copy the .addin file to one of these locations:
| Location | Scope |
|---|---|
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
- Always use
TransactionMode.Manualwhen your command modifies the model - Call
tx.Start()before any modifications andtx.Commit()after - Use a
usingblock so the transaction is disposed even if an exception occurs - Give each transaction a descriptive name - this becomes the undo label in Revit’s Edit menu
- Never nest transactions - use
SubTransactionorTransactionGroupfor complex multi-step operations - If something goes wrong, call
tx.RollBack()instead oftx.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
- Apply quick filters first.
OfClass()andOfCategory()are “quick filters” that run at the database level before elements are loaded into memory. Always use these before slower LINQ queries. - Avoid
.ToElements()when you only need a count. Use.GetElementCount()instead. - Scope to a view when possible. Passing a
viewIdto the collector limits the search to elements visible in that view, which is significantly faster for large models. - Combine filters with
IntersectionFilterinstead 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
- Add a
Resourcesfolder to your Visual Studio project - Add 32x32 and 16x16 PNG images for your button icons
- Set each image’s Build Action to
Resourcein the Properties panel - 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 Type | Read Method | Write Method |
|---|---|---|
String | AsString() | Set(string) |
Integer | AsInteger() | Set(int) |
Double | AsDouble() | Set(double) |
ElementId | AsElementId() | 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:
- 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.
- 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.
- 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.
| Choice | Effect | When to use it |
|---|---|---|
| Always Load | Revit stops asking about this add-in | Once the tool is stable and deployed to the team |
| Load Once | Loads for this session only | The right default while developing |
| Do Not Load | Skips the add-in entirely | When 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
- Build your project in Debug configuration
- Start Revit normally (not from Visual Studio)
- In Visual Studio, go to Debug > Attach to Process
- Find
Revit.exein the list and click Attach - Set breakpoints in your code
- 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
.addinfile at all, so check the year folder and theAssemblypath 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
- Create a folder for your plugin files (e.g.,
C:\RevitPlugins\MyTools\) - Copy your DLL and any dependency DLLs into this folder
- Copy the
.addinfile to the appropriate Addins folder - Make sure the
Assemblypath in the.addinfile 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
.addinfile in%ProgramData%\Autodesk\Revit\Addins\2025\ - Handle multiple Revit versions by placing
.addinfiles in each version’s folder - Include an uninstaller that removes both the DLL and
.addinfile
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 sharenet8.0-windows, and Revit 2027 needsnet10.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
.addinfile 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.
-
Not setting Copy Local to False on Revit API references. This causes assembly loading conflicts and crashes at startup.
-
Forgetting the Transaction attribute. Every
IExternalCommandclass must have[Transaction(TransactionMode.Manual)]or[Transaction(TransactionMode.ReadOnly)]. -
Using LINQ
.Count()instead of.GetElementCount(). The LINQ method loads all elements into memory first. The collector method counts at the database level. -
Hardcoding file paths. Use
Environment.GetFolderPath()and relative paths so your plugin works on different machines. -
Not handling the “no document open” case. Always check if
commandData.Application.ActiveUIDocumentis null before accessing the Document. -
Ignoring internal units. All measurements in the Revit API use feet (lengths) and radians (angles). Always convert for display.
-
Running long operations on the main thread without a progress bar. Use
IExternalEventHandlerandExternalEventfor asynchronous operations, and show aProgressBarfor operations that take more than a few seconds. -
Not disposing of
FilteredElementCollector. While the garbage collector will eventually clean it up, disposing it promptly releases database resources. -
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.
-
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.
-
Still using
ElementId.IntegerValueon Revit 2025 or later. It was removed in favour ofValue, which returns along. Widen the variables that hold it rather than casting back toint.
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
IExternalEventHandlerfor thread-safe model access - Updaters and events - react to model changes in real-time using
IUpdateror document events likeDocumentChanged - 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