Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions content/features/Useful-script-snippets.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
uid: useful-script-snippets
title: Useful script snippets
author: Daniel Otykier
updated: 2026-04-10
applies_to:
products:
- product: Tabular Editor 2
Expand All @@ -21,6 +22,9 @@ Here's a collection of small script snippets to get you started using the [Advan

Also, make sure to check out our script library @csharp-script-library, for some more real-life examples of what you can do with the scripting capabilities of Tabular Editor.

> [!TIP]
> For structured, pattern-by-pattern reference material on C# scripting and Dynamic LINQ, see the [Scripting Patterns](/how-tos/scripting-navigate-tom-hierarchy) how-to series. For the complete TOM wrapper API, see the @api-index.

***

## Create measures from columns
Expand Down
206 changes: 206 additions & 0 deletions content/how-tos/scripting-add-clone-remove-objects.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
---
uid: how-to-add-clone-remove-objects
title: How to Add, Clone and Remove Objects
author: Morten Lønskov
updated: 2026-04-10
applies_to:
products:
- product: Tabular Editor 2
full: true
- product: Tabular Editor 3
full: true
---
# How to Add, Clone and Remove Objects

C# scripts can create new model objects, clone existing ones and delete objects. This article covers the Add, Clone and Delete patterns.

## Quick reference

```csharp
Comment thread
mlonsk marked this conversation as resolved.
// Add objects -- all parameters after the first are optional.
// See sections below for parameter details.
table.AddMeasure("Name", "DAX Expression", "Display Folder");
Comment thread
mlonsk marked this conversation as resolved.
table.AddCalculatedColumn("Name", "DAX Expression", "Display Folder");
table.AddDataColumn("Name", "SourceColumn", "Display Folder", DataType.String);
table.AddHierarchy("Name", "Display Folder", col1, col2, col3);
Model.AddCalculatedTable("Name", "DAX Expression");
Model.AddPerspective("Name");
Model.AddRole("Name");
Model.AddTranslation("da-DK");

// Relationships
var rel = Model.AddRelationship();
rel.FromColumn = Model.Tables["Sales"].Columns["ProductKey"]; // many (N) side
rel.ToColumn = Model.Tables["Product"].Columns["ProductKey"]; // one (1) side

// Clone
var clone = measure.Clone("New Name"); // same table
var clone = measure.Clone("New Name", true, targetTable); // different table

// Delete (always materialize with ToList() before modifying a collection in a loop)
measure.Delete();
table.Measures.Where(m => m.IsHidden).ToList().ForEach(m => m.Delete());
```

## Adding measures

`AddMeasure()` creates and returns a new `Measure` on a table. The first parameter is the name, the second is a DAX expression and the third is the display folder. All parameters except the first are optional.

Capture the returned object in a variable to set additional properties. This pattern is the same across all `Add*` methods.

```csharp
var table = Model.Tables["Sales"];

// Create a measure and set properties on the returned object
var m = table.AddMeasure(
"Revenue", // name
"SUM('Sales'[Amount])" // DAX expression
);
m.FormatString = "#,##0.00";
m.Description = "Total sales amount";

// With display folder
var m2 = table.AddMeasure(
"Cost", // name
"SUM('Sales'[Cost])", // DAX expression
"Financial" // display folder
);
```

## Adding columns
Comment thread
mlonsk marked this conversation as resolved.

```csharp
// Calculated column -- first parameter is the name, second is a DAX expression
var cc = table.AddCalculatedColumn(
"Profit", // name
"'Sales'[Amount] - 'Sales'[Cost]" // DAX expression
);
cc.DataType = DataType.Decimal;
cc.FormatString = "#,##0.00";

// Data column -- maps to a source column in the partition query
var dc = table.AddDataColumn(
"Region", // name
"RegionName", // source column name
"Geography", // display folder
DataType.String // data type
);
```

> [!WARNING]
> Adding a data column does not modify the table's partition query. You must update the M expression or SQL query separately to include a source column that matches the `sourceColumn` parameter.

## Adding hierarchies

The `levels` parameter is variadic. Pass any number of columns in a single call to create the corresponding levels automatically.

```csharp
var dateTable = Model.Tables["Date"];
var h = dateTable.AddHierarchy(
"Calendar", // name
"", // display folder
dateTable.Columns["Year"], // level 1
dateTable.Columns["Quarter"], // level 2
dateTable.Columns["Month"] // level 3
);
```

Or add levels one at a time:

```csharp
var h = dateTable.AddHierarchy("Fiscal");
h.AddLevel(dateTable.Columns["FiscalYear"]);
h.AddLevel(dateTable.Columns["FiscalQuarter"]);
h.AddLevel(dateTable.Columns["FiscalMonth"]);
```

## Adding calculated tables
Comment thread
mlonsk marked this conversation as resolved.

```csharp
var ct = Model.AddCalculatedTable(
"DateKey List", // name
"VALUES('Date'[DateKey])" // DAX expression
);
```

## Adding relationships

`AddRelationship()` creates and returns an empty relationship. You must set the columns explicitly.

`FromColumn` is the many (N) side and `ToColumn` is the one (1) side. Tabular Editor does not detect the direction automatically. A useful mnemonic: F for From, F for Fact table (the many side).

New relationships default to `CrossFilteringBehavior.OneDirection` and `IsActive = true`. Set these only if you need a different value.

```csharp
var rel = Model.AddRelationship();
rel.FromColumn = Model.Tables["Sales"].Columns["ProductKey"]; // many side (fact)
rel.ToColumn = Model.Tables["Product"].Columns["ProductKey"]; // one side (dimension)

// Only set these if you need non-default values:
// rel.CrossFilteringBehavior = CrossFilteringBehavior.BothDirections;
// rel.IsActive = false;
```

## Cloning objects

`Clone()` creates a copy with all properties, annotations and translations.

```csharp
// Clone within the same table
var original = Model.AllMeasures.First(m => m.Name == "Revenue");
var copy = original.Clone("Revenue Copy");

// Clone to a different table (with translations)
var copy2 = original.Clone("Revenue Copy", true, Model.Tables["Reporting"]);
```

## Generating measures from columns

A common pattern: iterate selected columns and create derived measures. Note the use of `DaxObjectFullName` which returns the fully qualified, properly quoted DAX reference (e.g., `'Sales'[Amount]`) to avoid quoting errors.

```csharp
foreach (var col in Selected.Columns)
{
var m = col.Table.AddMeasure(
"Sum of " + col.Name,
"SUM(" + col.DaxObjectFullName + ")",
Comment thread
mlonsk marked this conversation as resolved.
col.DisplayFolder
);
m.FormatString = "0.00";
col.IsHidden = true;
}
```

## Deleting objects

Call `Delete()` on any named object to remove it. When modifying a collection in a loop (deleting, adding or moving objects), always call `.ToList()` first to materialize a snapshot.

```csharp
// Delete a single object
Model.AllMeasures.First(m => m.Name == "Temp").Delete();

// Delete multiple objects safely
Model.AllMeasures
.Where(m => m.HasAnnotation("DEPRECATED"))
.ToList()
.ForEach(m => m.Delete());
```

## Common pitfalls

> [!WARNING]
> - Always call `.ToList()` or `.ToArray()` before modifying objects in a loop. Without it, modifying the collection during iteration causes: `"Collection was modified; enumeration operation may not complete."`
> - `AddRelationship()` creates an incomplete relationship. You must assign both `FromColumn` and `ToColumn` before the model validates.
> - `Column` is abstract, but you can access all base properties (`Name`, `DataType`, `FormatString`, `IsHidden`) without casting. Only cast to a subtype for type-specific properties.
> - `Clone()` copies all metadata including annotations, translations and perspective membership. Remove unwanted metadata after cloning.

## See also

- @useful-script-snippets
- @script-create-sum-measures-from-columns
- @how-to-navigate-tom-hierarchy
- @how-to-use-selected-object
- (xref:TabularEditor.TOMWrapper.Measure) -- Measure API reference
- (xref:TabularEditor.TOMWrapper.Column) -- Column API reference
- (xref:TabularEditor.TOMWrapper.Hierarchy) -- Hierarchy API reference
- (xref:TabularEditor.TOMWrapper.SingleColumnRelationship) -- Relationship API reference
107 changes: 107 additions & 0 deletions content/how-tos/scripting-check-object-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
---
uid: how-to-check-object-types
title: How to Check Object Types
author: Morten Lønskov
updated: 2026-04-10
applies_to:
products:
- product: Tabular Editor 2
full: true
- product: Tabular Editor 3
full: true
---
# How to Check Object Types

The TOM hierarchy uses inheritance. `Column` is an abstract base with subtypes `DataColumn`, `CalculatedColumn` and `CalculatedTableColumn`. `Table` has subtypes `CalculatedTable` and `CalculationGroupTable`. Use the base type when working with shared properties like `Name`, `Description`, `IsHidden`, `FormatString` or `DisplayFolder`. Cast to a concrete subtype when you need type-specific properties, such as `Expression` on `CalculatedColumn` or `SourceColumn` on `DataColumn`.

## Quick reference
Comment thread
mlonsk marked this conversation as resolved.

```csharp
// Pattern matching -- checks type AND casts in one step
if (col is CalculatedColumn cc)
Comment thread
otykier marked this conversation as resolved.
Info(cc.Expression); // Expression is only on CalculatedColumn, not base Column

// Filter a collection by type
var calcCols = Model.AllColumns.OfType<CalculatedColumn>();
Comment thread
mlonsk marked this conversation as resolved.
var calcGroups = Model.Tables.OfType<CalculationGroupTable>();

// Runtime type name (use only for display/logging, not for logic)
string typeName = obj.GetType().Name; // "DataColumn", "Measure", etc.
Comment thread
mlonsk marked this conversation as resolved.
Outdated
```

> [!NOTE]
> Pattern matching with variable declaration (`col is CalculatedColumn cc`) requires the Roslyn compiler in Tabular Editor 2. Enable it under **File > Preferences > General > Use Roslyn compiler**. Tabular Editor 3 supports this by default.
Comment thread
mlonsk marked this conversation as resolved.
Outdated

## Type hierarchy

The key inheritance relationships in the TOM wrapper:
Comment thread
mlonsk marked this conversation as resolved.

| Base type | Subtypes |
|---|---|
| (xref:TabularEditor.TOMWrapper.Column) | (xref:TabularEditor.TOMWrapper.DataColumn), (xref:TabularEditor.TOMWrapper.CalculatedColumn), (xref:TabularEditor.TOMWrapper.CalculatedTableColumn) |
| (xref:TabularEditor.TOMWrapper.Table) | (xref:TabularEditor.TOMWrapper.CalculatedTable), (xref:TabularEditor.TOMWrapper.CalculationGroupTable) |
| (xref:TabularEditor.TOMWrapper.Partition) | (xref:TabularEditor.TOMWrapper.MPartition), (xref:TabularEditor.TOMWrapper.EntityPartition), (xref:TabularEditor.TOMWrapper.PolicyRangePartition) |
| (xref:TabularEditor.TOMWrapper.DataSource) | (xref:TabularEditor.TOMWrapper.ProviderDataSource), (xref:TabularEditor.TOMWrapper.StructuredDataSource) |

## Filtering collections by type

`OfType<T>()` works on any collection and returns a filtered sequence containing only items that are the specified type. It returns an empty sequence if no items match.

```csharp
// All calculated columns in the model (empty if model has none)
var calculatedColumns = Model.AllColumns.OfType<CalculatedColumn>();

// All M partitions (Power Query)
var mPartitions = Model.AllPartitions.OfType<MPartition>();

// All calculation group tables
var calcGroups = Model.Tables.OfType<CalculationGroupTable>();

// All regular tables (exclude calculation groups and calculated tables)
var regularTables = Model.Tables.Where(t => t is not CalculationGroupTable && t is not CalculatedTable);
```

## Pattern matching with is

Pattern matching does two things: it checks whether a value is a given type and optionally casts it into a new variable. The form `x is Type xx` asks "is `x` of type `Type`?" and, if true, gives you `xx` as a variable of that exact type.

This is equivalent to:

```csharp
if (col is CalculatedColumn)
{
var cc = (CalculatedColumn)col; // explicit cast
// use cc...
}
```

If you only need the boolean check, use `x is Type` without the variable. If you also need subtype-specific properties, use `x is Type xx`.

```csharp
foreach (var col in Model.AllColumns)
{
// Expression is only available on CalculatedColumn, not the base Column type
if (col is CalculatedColumn cc)
Info($"{cc.Name}: {cc.Expression}");
else if (col is DataColumn dc)
Info($"{dc.Name}: data column in {dc.Table.Name}");
}
```

## Dynamic LINQ equivalent

In BPA rules, type filtering is handled by the rule's **Applies to** scope. Set it to the target object type (e.g., **Calculated Columns**) rather than filtering by type in the expression. No C#-style type casting is available in Dynamic LINQ.

## Common pitfalls

> [!IMPORTANT]
> - `Column` is abstract, but you can access all properties defined on the base type (`Name`, `DataType`, `FormatString`, `IsHidden`, `Description`, `DisplayFolder`) without casting. Only cast to a subtype when you need subtype-specific properties like `Expression` on `CalculatedColumn`.
> - `OfType<T>()` both filters and casts. `Where(x => x is T)` only filters, leaving you with the base type. Prefer `OfType<T>()` when you need access to subtype properties.
> - Calculated table columns are managed automatically. Edit the calculated table's `Expression` to add or change columns. You cannot add them directly.

## See also

- @csharp-scripts
- @using-bpa-sample-rules-expressions
- @how-to-navigate-tom-hierarchy
- @how-to-tom-interfaces
Loading
Loading