ToDSType() ambiguity in Revit2027/C#

Stumbling into ZT node development and have hit a roadblock I can’t seem to figure out a good fix for.

I’m using Revit 2027 and have had issues getting ToDSType() to work for certain classes when attempting to convert DB Elements back into Revit.Elements.Element. It works perfectly fine for most classes I’ve tried it with such as Ceilings, Doors and Sheets, but for some reason will not work for the ImportInstance class - it returns a warning along the lines of:

Pkl_Collect.CadImports operation failed. The call is ambiguous between the following methods or properties: ‘Revit.Elements.ElementWrapper.Wrap(Autodesk.Revit.DB.AppearanceAssetElement, bool)’ and ‘Revit.Elements.ElementWrapper.Wrap(Autodesk.Revit.DB.RevitLinkInstance, bool)’

I understand what this means, and I think I know why it is happening - the Wrap method is trying to find a suitable overload, and can’t find one or isn’t defaulting to an Element based approach where it doesn’t try to work via the type inhereted from the Element class. What I don’t know is if this is a known limitation of working in C#/ZeroTouch or if I’m going about this incorrectly. I’ve had no issues getting Python to automatically wrap this for me in the past so assume it’s something I’m overlooking. From what I can see the ImportInstance class inherits from neither of the ambiguities so I have to assume it’s hitting a fallback overload and getting tripped up in resolving it.

I’ve tried various things such as not sending Dynamo Elements to canvas (which gives me DB Elements, not ideal), I’ve tried generic and explicitly Element based methods, using Linq/foreach approaches and also calling the Wrap method from its own namespace and using dynamic typing for type assessment at runtime. No bingo.

Any help appreciated, can share more code if needed.

Example of calling ToDsType() below:

/// <summary>
/// Converts a list of DB Elements to Dynamo Elements.
/// </summary>
/// <param name="revitElements">Elements to convert.</param>
/// <param name="revitOwned">Does Revit own the Elements.</param>
/// <returns>A list of Dynamo Elements.</returns>
internal static IList<DynElement> Ext_ToDynamoElements<T>(this IEnumerable<T> revitElements, bool revitOwned = false) where T : DB.Element
{
    return revitElements
        .Select(e => e.ToDSType(revitOwned))
        .ToList();
}
global using DynElement = global::Revit.Elements.Element;

I don’t have any direct answers just yet as I haven’t run into this before (everything I’ve done in the Revit context has been a custom object type not an expansion on existing classes), but I have a few minutes to think on this. I do have a few questions which might help get onto a path to resolution.

  1. Do you really intend to return a generic Element with whatever you’re passing, or would you want to return the elements as an instance of their respective class?
  2. Do you get the error with all classes, or is it unique to a particular class?
  3. Is this function just an example and not something you’re using consistently in your code? It’s likely faster to just use the ToDSType method in your references Dynamo for Revit code base.

Yep good queries, tried a few things related to them but mucking around with non-extended calls for now mainly in case it’s something coming through there as well.

Core code seems to work, just the dstype part I’m getting stuck on:

Main difference I can see is the Elements never become their actual class, they remain inherited from Element at all stages. Did try casting back, before making DSType but I’ll test further in case I’ve got my wires crossed. Mainly wanted to check there wasn’t a known limitation of ToDSType before delving too far.

  1. Forcing T was my moreorless trying to trick the DSType method after having it fail by seeing if I could avoid assumed inheritance. Tried with/without Element inheritance assumptions and got same outcome so far.
  2. This particular class so far.
  3. I’ve used the extension method elsewhere with it working, so far for Ceilings and Doors, example below
/// <summary>
/// Collects all ceilings in a linked instance's document or the current document if not provided.
/// </summary>
/// <param name="docOrLinkInstance ">Optional Revit linked instance to get ceilings from.</param>
/// <returns>A list of ceilings.</returns>
/// <search>collect, ceiling</search>
[MultiReturn("ceilings")]
public static Dictionary<string, object> GetAllCeilings([DefaultArgument("null")] object? docOrLinkInstance = null)
{
    // Get the related document
    DB.Document? doc = pklGen.GetDocumentRoutine(docOrLinkInstance, fallBack: true);
            
    // Output name
    string outputName1 = "ceilings";

    // Default output dictionary
    var output = new Dictionary<string, object>
    {
        { outputName1, new List<DynElement>() }
    };

    // Early return/warning if no document
    if (doc == null)
    {
        LogWarningMessageEvents.OnLogWarningMessage("Linked document could not be retrieved. Is the link loaded?");
        return output;
    }

    // Collect elements and return as output
    output[outputName1] = doc.Ext_CollectByCategoryToDyn(DB.BuiltInCategory.OST_Ceilings);
    return output;
}
/// <summary>
/// Collects DB Elements by BuiltInCategory, then converts them to Dynamo Elements.
/// </summary>
/// <param name="category">The category to filter by.</param>
/// <param name="doc">The DB Document.</param>
/// <param name="elementTypes">Collect ElementTypes.</param>
/// <param name="view">Optional view to limit collection to.</param>
/// <param name="revitOwned">If the Dynamo Elements are Revit owned.</param>
/// <returns>An IList of Dynamo Elements.</returns>
internal static IList<DynElement> Ext_CollectByCategoryToDyn(this DB.Document doc,
    DB.BuiltInCategory category, bool elementTypes = false, DB.View view = null, bool revitOwned = true)
{
    return doc.Ext_CollectByCategory(category, elementTypes: elementTypes, view: view)
        .Ext_ToDynamoElements(revitOwned);
}
/// <summary>
/// Collects DB Elements by BuiltInCategory.
/// </summary>
/// <param name="category">The category to filter by.</param>
/// <param name="doc">The DB Document.</param>
/// <param name="elementTypes">Collect ElementTypes.</param>
/// <param name="view">Optional view to limit collection to.</param>
/// <returns>An IList of DB Elements.</returns>
internal static IList<DB.Element> Ext_CollectByCategory(this DB.Document doc,
    DB.BuiltInCategory category, bool elementTypes = false, DB.View view = null)
{
    // Catch nulls / invalid Category
    if (doc == null || category == DB.BuiltInCategory.INVALID)
    {
        return new List<DB.Element>();
    }

    // Return DB Elements by category
    return doc.Ext_Collector(elementTypes: elementTypes, view: view)
        .OfCategory(category)
        .ToElements();
}

OK might help narrow the case down, I’ve found I can trigger this error off a basic ToDSType() on a View, where it is sometimes null. Maybe gives some hints… assumed ToDSType() can deal with a null…maybe not.

Hmmm…

If (seriously big if as I’m on a laptop in a train at the moment so my available screen is pretty limited) I am catching this right this is the call in question:

return doc.Ext_CollectByCategory(category, elementTypes: elementTypes, view: view).Ext_ToDynamoElements(revitOwned);

The first method (Ext_CollectByCategory) returns a IList of Revit.DB.Elements. The second method (Ext_ToDynamoElements) iterates over the list to return a Dynamo element.

I think the issue is that EVERYTHING in those methods is generic so the constructor is confused by what you’re providing and expecting in return when given a null object as there isn’t a readily identifiable object type. Chopping stuff into too many short methods can cause such confusion in my experience, and is a reason that I recommend avoiding conflicting overloads when possible.

Try something like this to see if that resolves it (again, on a train, not in VS so no autocomplete, so sorry for any issues with the code):
return doc.Ext_CollectByCategory(category, elementTypes: elementTypes, view: view).Select(x=>x.ToDSType(revitOwned));

Getting closer, suspect maybe it’s null based if not View being too ambiguous for the wrapper.

Will update as I go in case it helps others in future. Views being ViewPlan vs View maybe related too.. will report back.

In the case of the 3rd instance, is that import into the base model instead of into a particular view? If so I would expect that to fail and likely in this way. Not sure if you can use an if statement this way but you can try something like this to handle the potential null…

internal static IList<DynElement> Ext_ToDynamoElements<T>(this IEnumerable<T> revitElements, bool revitOwned = false) where T : DB.Element
{
    return revitElements
        .Select(e => e==null? null: e.ToDSType(revitOwned))
        .ToList();
}

Thanks, it was indeed the nulls. It was nice knowing you ToDsType(), time to wrap the wrapper.

Sort of feels like undocumented behavior in the error it throws, maybe one for dev team to consider as a (pretty common) edge case for null guarding? I assume it’s likely been considered so no worries if there’s reasons against it.

internal static class Ext_DBElement
{
    internal static DynElement? Ext_ToDynElement(this DB.Element element, bool revitOwned)
    {
        return element == null ? null : element.ToDSType(revitOwned);
    }
}

Seems related to 3dViewTemplates wrappers.

TLDR: If a wrapper doesn’t exist in the API, it will fail. Generally I return most elements as Revit.Elements.Element in Rhythm (I think).

I also think spring nodes had a workaround once showing how to custom cast a 3d view template with ToDsType().

Thanks! In this case the views were all ViewPlans and the null was throwing it off, I assume the 3D view thing is involved in the ambiguity you mean? Appreciate the link, fringe case to catch in my logic.