Preserving Backward Compatibility in Revit Extensions
Nice3point.Revit.Extensions is a public NuGet library with downstream consumers; its public surface is a contract.
A deleted or altered member breaks other people's builds. Keep the old member forever, mark it [Obsolete], and ship a JetBrains CodeTemplate; ReSharper and Rider then auto-migrate call sites to the replacement.
This skill covers deprecating, renaming, or superseding an existing member; it depends on JetBrains annotations (JetBrains.Annotations) for both [Obsolete]'s companion CodeTemplate and the surrounding attributes.
When to use
- Renaming an extension or replacing it with a better-named member while keeping every existing call compiling.
- Retiring a member whose underlying Revit API was deprecated in a specific Revit year.
- Deciding whether a proposed change to a signature, return type, or parameter list is allowed at all.
When not to use
- Adding a member that does not yet exist on the public surface — that is new-surface work; use revit-extensions-authoring.
Workflow
Step 1: Add the replacement beside the original, both wrapping the raw API
Keep the old member in place and add the new one in the same extension(Type) block.
Both members call the underlying Revit API directly; the new member never delegates to the old one, and the old one never delegates to the new one.
Write full method bodies; an expression-bodied property is fine for a read-only value.
extension(Element element)
{
/// <summary>Determines whether element can be mirrored</summary>
/// <returns>True if the element can be mirrored</returns>
public bool CanBeMirrored => ElementTransformUtils.CanMirrorElement(element.Document, element.Id);
}
Step 2: Mark the original [Obsolete] with a message that names the replacement
The message states plainly what to use instead. The obsolete member keeps calling the original Revit API; it stays correct and independent and must not recurse into the new extension.
[Pure]
[Obsolete("Use CanBeMirrored() instead")]
public bool CanMirrorElement()
{
return ElementTransformUtils.CanMirrorElement(element.Document, element.Id);
}
Step 3: Attach the CodeTemplate auto-conversion pattern
Add [CodeTemplate] beside [Obsolete] so the IDE rewrites call sites automatically.
searchTemplate matches the old call, ReplaceTemplate produces the new call, and the two Message/ReplaceMessage strings surface in the inspection and the quick-fix.
Use $expr$ for the receiver and match the replacement shape exactly — a property replacement drops the parentheses.
[Pure]
[Obsolete("Use CanBeMirrored() instead")]
[CodeTemplate(
searchTemplate: "$expr$.CanMirrorElement()",
Message = "CanMirrorElement is obsolete, use CanBeMirrored instead",
ReplaceTemplate = "$expr$.CanBeMirrored",
ReplaceMessage = "Replace with CanBeMirrored")]
public bool CanMirrorElement()
{
return ElementTransformUtils.CanMirrorElement(element.Document, element.Id);
}
When the member takes arguments, thread them through the template with named placeholders; the rewrite preserves them:
[Pure]
[Obsolete("Use CanBeMirrored() instead")]
[CodeTemplate(
searchTemplate: "$expr$.CanMirrorElements($document$)",
Message = "CanMirrorElements is obsolete, use CanBeMirrored instead",
ReplaceTemplate = "$expr$.CanBeMirrored($document$)",
ReplaceMessage = "Replace with CanBeMirrored()")]
public bool CanMirrorElements(Document document)
{
return ElementTransformUtils.CanMirrorElements(document, elements);
}
Step 4: Gate the deprecation when Revit deprecates the API in a specific year
If the underlying Revit API is only obsolete from a given release, wrap just the [Obsolete] and [CodeTemplate] attributes in the matching #if; the member deprecates on the versions that warrant it and stays clean elsewhere.
When the replacement adds a parameter, add it as an optional argument at the end; use a $placeholder$ in the template for the new argument.
[Pure]
#if REVIT2027_OR_GREATER
[Obsolete("This method is deprecated in Revit 2027 and may be removed in a later version of Revit. We suggest you use the overload which accepts a region input instead.")]
[CodeTemplate(
searchTemplate: "$expr$.DownloadParameterOptions()",
Message = "DownloadParameterOptions() is obsolete, use overload with region parameter",
ReplaceTemplate = "$expr$.DownloadParameterOptions($arg$)",
ReplaceMessage = "Replace with DownloadParameterOptions(region)")]
#endif
public ParameterDownloadOptions DownloadParameterOptions()
{
return ParameterUtils.DownloadParameterOptions(typeId);
}
Step 5: Confirm nothing on the surface actually broke
Hold every change to the allowed set before you compile:
- Never change an existing signature or return type — the only allowed return-type change is a
voidmutation becoming its source object for chaining. - Add new parameters only as optional, and only at the end of the list.
- Keep the old member functional indefinitely; deprecation is not deletion.
Then compile every configuration from the build directory and update the shipped docs in the same change.
dotnet run -c Release
Update Readme.md, add a categorized Changelog.md entry for the deprecation, and keep the XML docs on both the old and new members.
Validation
- The original member still exists, still compiles, and still calls the original Revit API, not the new member.
-
[Obsolete]carries a message that names the replacement; no[EditorBrowsable(EditorBrowsableState.Never)]sits on an obsoleteElementextension. -
[CodeTemplate]suppliessearchTemplate,ReplaceTemplate,Message, andReplaceMessage, and the replacement shape matches the new member (property vs. method, argument placeholders preserved). - A Revit-year-specific deprecation wraps only the attributes in the matching
#if. - No signature or return type changed; any new parameter is optional and last;
dotnet run -c Releasecompiles every configuration andReadme.md/Changelog.md/XML docs are updated.
Common Pitfalls
| Pitfall | Correct approach |
|---|---|
| Deleting or renaming the old member outright | Keep it, mark it [Obsolete], and add the replacement beside it. |
| The obsolete member calling the new extension | Call the original Revit API from the obsolete member; it never recurses and stays independent. |
[EditorBrowsable(Never)] on an obsolete Element extension |
Omit it; let the member stay discoverable while [Obsolete] warns. |
[Obsolete] without a [CodeTemplate] |
Add the searchTemplate → ReplaceTemplate pattern; callers auto-migrate. |
ReplaceTemplate keeping parentheses for a property replacement |
Match the replacement exactly — drop the () when the new member is a property. |
| Changing a signature or return type, or inserting a required parameter | Add a new member or an optional trailing parameter instead. |
| Deprecating unconditionally when Revit deprecates only in a later year | Wrap just the [Obsolete]/[CodeTemplate] attributes in the matching #if. |