Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
// Add objects -- all parameters after the first are optional.
// See sections below for parameter details.
table.AddMeasure("Name", "DAX Expression", "Display Folder");
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

```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

```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 + ")",
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

```csharp
// Pattern matching -- checks type AND casts in one step
if (col is CalculatedColumn cc)
Info(cc.Expression); // Expression is only on CalculatedColumn, not base Column

// Filter a collection by type
var calcCols = Model.AllColumns.OfType<CalculatedColumn>();
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.
```

> [!NOTE]
> Pattern matching with variable declaration (`col is CalculatedColumn cc`) requires the Roslyn compiler in Tabular Editor 2. Configure it under **File > Preferences > General > Compiler path**. See [Compiling with Roslyn](xref:advanced-scripting#compiling-with-roslyn) for details. Tabular Editor 3 supports this by default.

## Type hierarchy

The key inheritance relationships in the TOM wrapper:

| 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