Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
3 changes: 3 additions & 0 deletions content/features/Useful-script-snippets.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,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. It covers object navigation, type checking, LINQ querying, dependencies, annotations, expressions and more.
Comment thread
mlonsk marked this conversation as resolved.
Outdated

***

## Create measures from columns
Expand Down
172 changes: 172 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,172 @@
---
uid: how-to-add-clone-remove-objects
title: How to Add, Clone and Remove Objects
author: Morten Lønskov
updated: 2026-04-09
Comment thread
mlonsk marked this conversation as resolved.
Outdated
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
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"];
rel.ToColumn = Model.Tables["Product"].Columns["ProductKey"];

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

// Delete (always materialize first when deleting in a loop)
Comment thread
mlonsk marked this conversation as resolved.
Outdated
measure.Delete();
table.Measures.Where(m => m.IsHidden).ToList().ForEach(m => m.Delete());
```

## Adding measures

`AddMeasure()` creates a new measure on a table. All parameters except the first are optional.
Comment thread
mlonsk marked this conversation as resolved.
Outdated

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

// Simple measure
var m = table.AddMeasure("Revenue", "SUM('Sales'[Amount])");
m.FormatString = "#,##0.00";
m.Description = "Total sales amount";

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

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

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

// Data column (maps to a source column)
var dc = table.AddDataColumn("Region", "RegionName", "Geography", DataType.String);
Comment thread
mlonsk marked this conversation as resolved.
Outdated
```

## Adding hierarchies

Pass columns as parameters to automatically create levels.
Comment thread
mlonsk marked this conversation as resolved.
Outdated

```csharp
var dateTable = Model.Tables["Date"];
var h = dateTable.AddHierarchy(
"Calendar",
"",
dateTable.Columns["Year"],
dateTable.Columns["Quarter"],
dateTable.Columns["Month"]
);
```

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", "VALUES('Date'[DateKey])");
```

## Adding relationships

`AddRelationship()` creates an empty relationship. You must set the columns explicitly.
Comment thread
mlonsk marked this conversation as resolved.
Outdated

```csharp
var rel = Model.AddRelationship();
rel.FromColumn = Model.Tables["Sales"].Columns["ProductKey"];
rel.ToColumn = Model.Tables["Product"].Columns["ProductKey"];
rel.CrossFilteringBehavior = CrossFilteringBehavior.OneDirection;
Comment thread
mlonsk marked this conversation as resolved.
Outdated
rel.IsActive = true;
```

## 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.

```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 deleting inside a loop, always call `.ToList()` first to avoid modifying the collection during iteration.

```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()` before deleting objects in a loop. Without it, modifying the collection during iteration causes an exception.
Comment thread
mlonsk marked this conversation as resolved.
Outdated
> - `AddRelationship()` creates an incomplete relationship. You must assign both `FromColumn` and `ToColumn` before the model validates. Failing to do so results in a validation error.
> - New objects have default property values. Set `DataType`, `FormatString`, `IsHidden` and other properties explicitly after creation.
Comment thread
mlonsk marked this conversation as resolved.
Outdated
> - `Clone()` copies all metadata including annotations, translations and perspective membership. If you do not want to inherit these, remove them after cloning.

## See also

- @useful-script-snippets
- @script-create-sum-measures-from-columns
- @how-to-navigate-tom-hierarchy
- @how-to-use-selected-object
142 changes: 142 additions & 0 deletions content/how-tos/scripting-check-object-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
---
uid: how-to-check-object-types
title: How to Check Object Types
author: Morten Lønskov
updated: 2026-04-09
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 the subtype `CalculationGroupTable`. This article shows how to test and filter by type in C# scripts and Dynamic LINQ.
Comment thread
mlonsk marked this conversation as resolved.
Outdated
Comment thread
mlonsk marked this conversation as resolved.
Outdated

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

```csharp
// Pattern matching (preferred)
if (col is CalculatedColumn cc)
Comment thread
otykier marked this conversation as resolved.
Info(cc.Expression);
Comment thread
mlonsk marked this conversation as resolved.
Outdated

// 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
string typeName = obj.GetType().Name; // "DataColumn", "Measure", etc.
Comment thread
mlonsk marked this conversation as resolved.
Outdated

// Null-safe cast
var dc = col as DataColumn;
Comment thread
mlonsk marked this conversation as resolved.
Outdated
if (dc != null) { /* use dc */ }
```

## Type hierarchy

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

| Base type | Subtypes |
|---|---|
| `Column` | `DataColumn`, `CalculatedColumn`, `CalculatedTableColumn` |
| `Table` | `CalculatedTable`, `CalculationGroupTable` |
| `Partition` | `MPartition`, `EntityPartition`, `PolicyRangePartition` |
| `DataSource` | `ProviderDataSource`, `StructuredDataSource` |

## Filtering collections by type

`OfType<T>()` filters and casts in one step. Prefer it over `Where(x => x is T)`.

```csharp
// All calculated columns in the model
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)
var regularTables = Model.Tables.Where(t => t is not CalculationGroupTable);
```

## Pattern matching with is

Use C# pattern matching to test and cast in a single expression.

```csharp
foreach (var col in Model.AllColumns)
{
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}");
}
```

## Checking ObjectType enum

Every TOM object has an `ObjectType` property that returns an enum value. This identifies the base type, not the subtype.

```csharp
foreach (var obj in Selected.Objects)
{
switch (obj.ObjectType)
{
case ObjectType.Measure: /* ... */ break;
case ObjectType.Column: /* ... */ break;
case ObjectType.Table: /* ... */ break;
case ObjectType.Hierarchy: /* ... */ break;
}
}
```

> [!WARNING]
Comment thread
mlonsk marked this conversation as resolved.
Outdated
> `ObjectType` does not distinguish subtypes. A `CalculatedColumn` and a `DataColumn` both return `ObjectType.Column`. Use `is` or `OfType<T>()` when you need subtype-level checks.

## Checking partition source type

For partitions, use the `SourceType` property to distinguish between storage modes without type-casting.
Comment thread
mlonsk marked this conversation as resolved.
Outdated

```csharp
foreach (var p in Model.AllPartitions)
{
switch (p.SourceType)
{
case PartitionSourceType.M: /* Power Query */ break;
case PartitionSourceType.Calculated: /* DAX calc table */ break;
case PartitionSourceType.Entity: /* Direct Lake */ break;
case PartitionSourceType.Query: /* Legacy SQL */ break;
}
}
```

## Dynamic LINQ equivalent

In BPA rules, type filtering works differently. Set the rule's **Applies to** scope to target a specific object type. Within the expression, use `ObjectTypeName` for the base type name as a string.

```
// BPA expression context: the rule "Applies to" determines the object type
// No C#-style type casting is available in Dynamic LINQ

// Check the object type name (rarely needed since scope handles this)
ObjectTypeName = "Measure"
ObjectTypeName = "Column"
```

For subtypes like calculated columns, set the BPA rule scope to **Calculated Columns** rather than trying to filter by type in the expression.

## Common pitfalls

> [!IMPORTANT]
> - `Column` is abstract. You cannot create an instance of `Column` directly. Use `table.AddDataColumn()` or `table.AddCalculatedColumn()` on regular tables. `AddCalculatedTableColumn()` is only available on `CalculatedTable`.
Comment thread
mlonsk marked this conversation as resolved.
Outdated
Comment thread
mlonsk marked this conversation as resolved.
Outdated
> - `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.
> - `ObjectType` and `SourceType` are base-level checks. For subtype-specific logic (e.g., accessing `Expression` on a `CalculatedColumn`), use `is` or `OfType<T>()`.

## See also

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