Why Your Solar AutoLISP Routines Break on AutoCAD 2024+ (And How to Migrate to .NET)
AutoCAD 2024 quietly broke selection set calls that worked since 2003. Here's what changed, why (command) is 66x slower than entmake, and the migration path solar engineers actually need.
Why Your Solar AutoLISP Routines Break on AutoCAD 2024+ (And How to Migrate to .NET)
On CADTutor, someone posted: "So I have been using this LISP routine for close to 20 years. It was working in 2024. This last month I am getting this error." The code hadn't changed. AutoCAD had.
If you just upgraded and a routine that drew solar strings or placed panel tags for the past decade is now dead, here's what changed and how to get unstuck.
AutoCAD 2024 changed the security model and nobody warned you
AutoCAD 2024 tightened Trusted Locations and changed how APPLOAD handles .LSP files. If your routine depends on a .dcl dialog file that's not in an explicitly trusted path, AutoCAD now silently exits the routine with error: quit / exit abort instead of telling you why. Same symptom if Windows Defender has decided your support folder is suspicious after a security update.
The SECURELOAD system variable now defaults to 2 (load only trusted content), which means any routine not in your trusted paths simply won't load — and the error message is often just "Impossible to load."
But that's not the only thing that broke.
AutoCAD 2024 silently changed the DIMZIN default to 8, which suppresses trailing zeros. Any LISP routine that calls (rtos layoutwidth 2 2) and expects "1189.00" now gets back "1189". If your stringing routine parses that string to calculate cable distances or build a schedule, it's silently wrong — no error, just corrupted math downstream.
Then there's the command resolution change: user-defined command names can now lose to AutoCAD built-ins under certain session states. A thread on the Autodesk forum describes a routine loaded successfully, with TLEN silently running the native Lengthen command instead.
If you're on AutoCAD 2025 or 2026 and wondering what else is coming: the 2025 release moved from .NET Framework 4.x to .NET 8. Cadguardian's migration guide puts it plainly: "If you treat 2025 like 'just another yearly upgrade,' your plugins will break." That warning is for .NET plugins. For LISP routines, there's no warning at all — they just stop.
The performance ceiling you've already been hitting
Before getting into migration, it's worth naming the other reason engineers end up here: the routines are slow.
The benchmark is from cadpanacea.com on drawing 10,000 lines:
(entmake): 0.17 seconds(vla-add-line): 1.10 seconds (~6x slower)(command)with CMDECHO off: 5.20 seconds (~30x slower)(command)with CMDECHO on: 11.3 seconds (~66x slower)
Most "slow" LISP routines use (command) in a loop. If your string-drawing routine picks two panel endpoints and runs (command "_.LINE" panelStart stringEnd "") for every connection, you already know what this feels like on a 500-panel commercial rooftop.
If you're using Civil 3D (which is where solar site work actually lives), benchmark data from CADTutor shows the same LISP code running 2.5x slower in Civil 3D 2017 than in vanilla AutoCAD 2015. Not a faster machine. Not a bigger drawing. Just Civil 3D.
The fix inside LISP is switching from (command) to (entmake) — but that's a rewrite anyway, and it only buys you so much. Lee Mac's selection set processing notes describe the hard ceiling: an AutoLISP application cannot have more than 128 selection sets open at once. On a large solar layout with per-row or per-string tracking, that ceiling is real.
Why you're emotionally correct to resist this
The folder of .lsp files you've been running since AutoCAD 2010 is not a liability. You built those routines because no commercial tool fit your workflow.
AutoLISP let engineers automate without becoming software developers. A 40-line routine to draw string polylines between selected panels — an afternoon's work, deployed by 5pm, no Visual Studio, no IT department. That immediacy was real.
The problem is that Autodesk stopped developing AutoLISP after 1995. Dynamic blocks in 2006, the modern properties palette, .NET 8 in 2025 — all of that happened at a layer LISP cannot reach. The interpreter still ships, so the routines still run. But they run slower every version, break more often, and dead-end before the features commercial solar automation actually needs.
The migration trap: translating LISP to C#
The most common mistake is trying to translate your LISP routine line by line into C#. Experienced AutoCAD developers describe this directly. From the Autodesk .NET forums:
Here's why that matters in practice. In LISP, entity data is a flat list of dotted pairs — group code 10 is the insert point, code 8 is the layer, code 2 is the block name. You memorize the DXF reference or you paste the codes from Lee Mac. It works, but every routine is a wall of magic numbers only the original author can read.
C# doesn't have group codes. It has blockRef.Position, blockRef.Layer, blockRef.Name — typed properties with IntelliSense. The mental model is completely different and a mechanical translation produces code that fights both paradigms at once.
Rewrite the problem, not the code
Don't translate. Rewrite the problem, not the code.
Here's a LISP routine that draws a string polyline between two picked panel blocks and calculates the cable distance:
; Draw a string between two selected solar panels
; Returns cable length in drawing units
(defun C:DRAWSTRING (/ p1 p2 e1 e2 pt1 pt2 dist)
(setvar "CMDECHO" 0)
(prompt "\nPick start panel: ")
(setq e1 (car (entsel)))
(setq pt1 (cdr (assoc 10 (entget e1)))) ; insert point of first panel
(prompt "\nPick end panel: ")
(setq e2 (car (entsel)))
(setq pt2 (cdr (assoc 10 (entget e2)))) ; insert point of second panel
; Draw the string polyline on SOLAR-STRINGS layer
(command "_.LAYER" "_M" "SOLAR-STRINGS" "")
(command "_.PLINE" pt1 pt2 "")
; Calculate and report cable length
(setq dist (distance pt1 pt2))
(prompt (strcat "\nCable distance: " (rtos dist 2 2) " units"))
dist
)
Straightforward. But it uses (command) — the slow path — and it breaks if AutoCAD processes the Trusted Locations check differently on the next version.
Here's the same problem rewritten in C# for AutoCAD .NET:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;
[assembly: CommandClass(typeof(SolarPlugin.StringingCommands))]
namespace SolarPlugin
{
public class StringingCommands
{
[CommandMethod("DRAWSTRING")]
public void DrawString()
{
// "Editor" is how you talk to the active drawing session
// Think of it as AutoCAD's input/output terminal
var doc = Application.DocumentManager.MdiActiveDocument;
var db = doc.Database;
var ed = doc.Editor;
// Pick the start panel block reference
var startResult = ed.GetEntity("\nPick start panel: ");
if (startResult.Status != PromptStatus.OK) return;
// Pick the end panel block reference
var endResult = ed.GetEntity("\nPick end panel: ");
if (endResult.Status != PromptStatus.OK) return;
// A Transaction is how you open the database for writing
// Think of it as an undo group — commit to apply, abort to cancel
using (var tr = db.TransactionManager.StartTransaction())
{
// Get the two panel block references
var startPanel = tr.GetObject(startResult.ObjectId,
OpenMode.ForRead) as BlockReference;
var endPanel = tr.GetObject(endResult.ObjectId,
OpenMode.ForRead) as BlockReference;
if (startPanel == null || endPanel == null)
{
ed.WriteMessage("\nSelected objects are not block references.");
return;
}
var stringStart = startPanel.Position; // 3D insert point
var stringEnd = endPanel.Position;
// The BlockTable is the catalog of all block definitions in your drawing
// Think of it as a master library — BlockTableRecord is one entry in that library
var bt = tr.GetObject(db.BlockTableId,
OpenMode.ForRead) as BlockTable;
var modelSpace = tr.GetObject(bt[BlockTableRecord.ModelSpace],
OpenMode.ForWrite) as BlockTableRecord;
// Create the string polyline directly via entmake equivalent
// No (command) call — this is the fast path
var stringPoly = new Polyline();
stringPoly.AddVertexAt(0, new Point2d(stringStart.X, stringStart.Y), 0, 0, 0);
stringPoly.AddVertexAt(1, new Point2d(stringEnd.X, stringEnd.Y), 0, 0, 0);
stringPoly.Layer = "SOLAR-STRINGS";
modelSpace.AppendEntity(stringPoly);
tr.AddNewlyCreatedDBObject(stringPoly, true);
// Calculate cable distance
double cableLength = stringStart.DistanceTo(stringEnd);
ed.WriteMessage($"\nCable distance: {cableLength:F2} drawing units");
tr.Commit(); // Without this, nothing happens
}
}
}
}
About 3x more lines. But notice what's different: no (command) calls, no layer-switching via command string, no DXF group codes. The geometry is created by constructing a Polyline object and appending it to model space inside a transaction. It's faster, it doesn't depend on CMDECHO, and it won't break because Autodesk changed the trusted-paths security model.
The three .NET concepts that bite engineers
Skip the OOP theory. Three concepts will bite you first:
Transactions. Think of them as undo groups. Everything you change in the drawing has to happen inside a using (var tr = db.TransactionManager.StartTransaction()) block. If you call tr.Commit(), the changes apply. If you forget, or if you call tr.Abort(), nothing happens. The most common beginner error is modifying an entity and seeing no change in the drawing — it's almost always a missing Commit. There's a thread on the Autodesk forums where this trips up almost every LISP developer making the jump.
The Editor. doc.Editor is AutoCAD's input/output. ed.GetEntity(), ed.GetPoint(), ed.WriteMessage() — this is how you get input from the user and write back to the command line. The return values from GetEntity and GetPoint have a .Status property you must check before using the result. PromptStatus.OK means the user picked something. PromptStatus.Cancel means they pressed Escape. Skipping this check is how you get null reference errors.
Pre-warn: Editor.GetSelection does not return a list. It returns a PromptSelectionResult. You cannot iterate over it directly — call .Value to get the SelectionSet, then iterate that. The error you get when you skip this step is not helpful. Hit this on the Autodesk forums before you hit it in your code.
The BlockTable. Think of it as the drawing's catalog of block definitions. SOLAR_PANEL_400W is a key in that catalog. The definition is a BlockTableRecord. Every inserted copy is a BlockReference pointing back to it. Your LISP version probably uses (ssget "_X" '((0 . "INSERT") (2 . "SOLAR_PANEL_400W"))). The .NET equivalent is ed.SelectAll(new SelectionFilter(...)) or walking model space manually.
If you run into dynamic panel blocks and blockRef.Name returns *U6 instead of SOLAR_PANEL_400W, that's the EffectiveName problem — CADTutor has the LISP workaround and the .NET equivalent is blockRef.GetEffectiveName().
Where the answers actually live
Start with Kean Walmsley's "Through the Interface" blog. Kean was Autodesk's developer advocate for years and his .NET posts from 2007–2018 are still the clearest explanations of transactions, selection, and entity creation available. Dense, searchable, authoritative.
AfraLisp's .NET section approaches things from a LISP developer's perspective — which LISP patterns don't translate, and what to do instead. The Autodesk .NET Developer's Guide is the canonical API reference.
When you get stuck — and you will — theswamp.org is the right forum. The regulars have been doing AutoCAD .NET development since the 2000s and won't dismiss you for coming from LISP.
What you're signing up for
Plan for weeks, not days. Your first working plugin will get rewritten once you understand transactions properly. And you'll be asking questions in places that won't dismiss you for not knowing what a using statement is.
The development feedback loop is slower than LISP. In LISP you type a routine and test it in 30 seconds. In .NET you write a class, build the .dll, copy it to AutoCAD's load path, restart AutoCAD, and then test. A post-build event that auto-copies the .dll helps a lot — set that up on day one.
The stability trade is worth it. A well-written .NET plugin survives AutoCAD version upgrades without accumulating silent regressions. Debugging in Visual Studio — real breakpoints, variable inspection, step-through — is a different category from (progn (alert "error here")).
The iceberg underneath
If you successfully migrate your string-drawing routine, you'll be looking at the tip of what your LISP folder has always been. The rest of it — voltage window validation per string, jumper cable handling, MPPT input balancing, NEC compliance by jurisdiction, cable schedule generation, homerun routing optimization, tag generation with correct circuit IDs — none of that is in the 50-line LISP routine you started with.
Each of those is a week-long project in .NET. We know because we've built all of them into Branch. The voltage window logic alone took two months once we got real drawings with non-standard module specs. Homerun routing required implementing K-means before we even got to the AutoCAD drawing part.
If you want to do the migration yourself, the path above is real. If you'd rather skip the two-year rebuild and get back to engineering work, try Branch free for 14 days.
Your LISP routines weren't wrong. They were the right tool for the job AutoLISP was designed for in 1995. The job has changed.
Going deeper: AutoLISP Scripts vs Solar Plugins covers when custom scripts still make sense vs. when a plugin pays off. For a current landscape view, the 5 best AutoCAD solar stringing plugins of 2026 compares the full field.