Skip to content

Commit 5e301fe

Browse files
committed
updates based on gregs comments
1 parent d80c8d1 commit 5e301fe

14 files changed

Lines changed: 376 additions & 213 deletions

content/features/Useful-script-snippets.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
uid: useful-script-snippets
33
title: Useful script snippets
44
author: Daniel Otykier
5+
updated: 2026-04-10
56
applies_to:
67
products:
78
- product: Tabular Editor 2
@@ -22,7 +23,7 @@ Here's a collection of small script snippets to get you started using the [Advan
2223
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.
2324

2425
> [!TIP]
25-
> 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.
26+
> 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.
2627
2728
***
2829

content/how-tos/scripting-add-clone-remove-objects.md

Lines changed: 65 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
uid: how-to-add-clone-remove-objects
33
title: How to Add, Clone and Remove Objects
44
author: Morten Lønskov
5-
updated: 2026-04-09
5+
updated: 2026-04-10
66
applies_to:
77
products:
88
- product: Tabular Editor 2
@@ -17,7 +17,8 @@ C# scripts can create new model objects, clone existing ones and delete objects.
1717
## Quick reference
1818

1919
```csharp
20-
// Add objects
20+
// Add objects -- all parameters after the first are optional.
21+
// See sections below for parameter details.
2122
table.AddMeasure("Name", "DAX Expression", "Display Folder");
2223
table.AddCalculatedColumn("Name", "DAX Expression", "Display Folder");
2324
table.AddDataColumn("Name", "SourceColumn", "Display Folder", DataType.String);
@@ -29,58 +30,78 @@ Model.AddTranslation("da-DK");
2930

3031
// Relationships
3132
var rel = Model.AddRelationship();
32-
rel.FromColumn = Model.Tables["Sales"].Columns["ProductKey"];
33-
rel.ToColumn = Model.Tables["Product"].Columns["ProductKey"];
33+
rel.FromColumn = Model.Tables["Sales"].Columns["ProductKey"]; // many (N) side
34+
rel.ToColumn = Model.Tables["Product"].Columns["ProductKey"]; // one (1) side
3435
3536
// Clone
3637
var clone = measure.Clone("New Name"); // same table
3738
var clone = measure.Clone("New Name", true, targetTable); // different table
3839
39-
// Delete (always materialize first when deleting in a loop)
40+
// Delete (always materialize with ToList() before modifying a collection in a loop)
4041
measure.Delete();
4142
table.Measures.Where(m => m.IsHidden).ToList().ForEach(m => m.Delete());
4243
```
4344

4445
## Adding measures
4546

46-
`AddMeasure()` creates a new measure on a table. All parameters except the first are optional.
47+
`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.
48+
49+
Capture the returned object in a variable to set additional properties. This pattern is the same across all `Add*` methods.
4750

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

51-
// Simple measure
52-
var m = table.AddMeasure("Revenue", "SUM('Sales'[Amount])");
54+
// Create a measure and set properties on the returned object
55+
var m = table.AddMeasure(
56+
"Revenue", // name
57+
"SUM('Sales'[Amount])" // DAX expression
58+
);
5359
m.FormatString = "#,##0.00";
5460
m.Description = "Total sales amount";
5561

5662
// With display folder
57-
var m2 = table.AddMeasure("Cost", "SUM('Sales'[Cost])", "Financial");
63+
var m2 = table.AddMeasure(
64+
"Cost", // name
65+
"SUM('Sales'[Cost])", // DAX expression
66+
"Financial" // display folder
67+
);
5868
```
5969

6070
## Adding columns
6171

6272
```csharp
63-
// Calculated column (DAX expression)
64-
var cc = table.AddCalculatedColumn("Profit", "'Sales'[Amount] - 'Sales'[Cost]");
73+
// Calculated column -- first parameter is the name, second is a DAX expression
74+
var cc = table.AddCalculatedColumn(
75+
"Profit", // name
76+
"'Sales'[Amount] - 'Sales'[Cost]" // DAX expression
77+
);
6578
cc.DataType = DataType.Decimal;
6679
cc.FormatString = "#,##0.00";
6780

68-
// Data column (maps to a source column)
69-
var dc = table.AddDataColumn("Region", "RegionName", "Geography", DataType.String);
81+
// Data column -- maps to a source column in the partition query
82+
var dc = table.AddDataColumn(
83+
"Region", // name
84+
"RegionName", // source column name
85+
"Geography", // display folder
86+
DataType.String // data type
87+
);
7088
```
7189

90+
> [!WARNING]
91+
> 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.
92+
7293
## Adding hierarchies
7394

74-
Pass columns as parameters to automatically create levels.
95+
The `levels` parameter is variadic. Pass any number of columns in a single call to create the corresponding levels automatically.
7596

7697
```csharp
7798
var dateTable = Model.Tables["Date"];
7899
var h = dateTable.AddHierarchy(
79-
"Calendar",
80-
"",
81-
dateTable.Columns["Year"],
82-
dateTable.Columns["Quarter"],
83-
dateTable.Columns["Month"]
100+
"Calendar", // name
101+
"", // display folder
102+
dateTable.Columns["Year"], // level 1
103+
dateTable.Columns["Quarter"], // level 2
104+
dateTable.Columns["Month"] // level 3
84105
);
85106
```
86107

@@ -96,19 +117,28 @@ h.AddLevel(dateTable.Columns["FiscalMonth"]);
96117
## Adding calculated tables
97118

98119
```csharp
99-
var ct = Model.AddCalculatedTable("DateKey List", "VALUES('Date'[DateKey])");
120+
var ct = Model.AddCalculatedTable(
121+
"DateKey List", // name
122+
"VALUES('Date'[DateKey])" // DAX expression
123+
);
100124
```
101125

102126
## Adding relationships
103127

104-
`AddRelationship()` creates an empty relationship. You must set the columns explicitly.
128+
`AddRelationship()` creates and returns an empty relationship. You must set the columns explicitly.
129+
130+
`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).
131+
132+
New relationships default to `CrossFilteringBehavior.OneDirection` and `IsActive = true`. Set these only if you need a different value.
105133

106134
```csharp
107135
var rel = Model.AddRelationship();
108-
rel.FromColumn = Model.Tables["Sales"].Columns["ProductKey"];
109-
rel.ToColumn = Model.Tables["Product"].Columns["ProductKey"];
110-
rel.CrossFilteringBehavior = CrossFilteringBehavior.OneDirection;
111-
rel.IsActive = true;
136+
rel.FromColumn = Model.Tables["Sales"].Columns["ProductKey"]; // many side (fact)
137+
rel.ToColumn = Model.Tables["Product"].Columns["ProductKey"]; // one side (dimension)
138+
139+
// Only set these if you need non-default values:
140+
// rel.CrossFilteringBehavior = CrossFilteringBehavior.BothDirections;
141+
// rel.IsActive = false;
112142
```
113143

114144
## Cloning objects
@@ -126,7 +156,7 @@ var copy2 = original.Clone("Revenue Copy", true, Model.Tables["Reporting"]);
126156

127157
## Generating measures from columns
128158

129-
A common pattern: iterate selected columns and create derived measures.
159+
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.
130160

131161
```csharp
132162
foreach (var col in Selected.Columns)
@@ -143,7 +173,7 @@ foreach (var col in Selected.Columns)
143173

144174
## Deleting objects
145175

146-
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.
176+
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.
147177

148178
```csharp
149179
// Delete a single object
@@ -159,14 +189,18 @@ Model.AllMeasures
159189
## Common pitfalls
160190

161191
> [!WARNING]
162-
> - Always call `.ToList()` before deleting objects in a loop. Without it, modifying the collection during iteration causes an exception.
163-
> - `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.
164-
> - New objects have default property values. Set `DataType`, `FormatString`, `IsHidden` and other properties explicitly after creation.
165-
> - `Clone()` copies all metadata including annotations, translations and perspective membership. If you do not want to inherit these, remove them after cloning.
192+
> - 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."`
193+
> - `AddRelationship()` creates an incomplete relationship. You must assign both `FromColumn` and `ToColumn` before the model validates.
194+
> - `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.
195+
> - `Clone()` copies all metadata including annotations, translations and perspective membership. Remove unwanted metadata after cloning.
166196
167197
## See also
168198

169199
- @useful-script-snippets
170200
- @script-create-sum-measures-from-columns
171201
- @how-to-navigate-tom-hierarchy
172202
- @how-to-use-selected-object
203+
- (xref:TabularEditor.TOMWrapper.Measure) -- Measure API reference
204+
- (xref:TabularEditor.TOMWrapper.Column) -- Column API reference
205+
- (xref:TabularEditor.TOMWrapper.Hierarchy) -- Hierarchy API reference
206+
- (xref:TabularEditor.TOMWrapper.SingleColumnRelationship) -- Relationship API reference

content/how-tos/scripting-check-object-types.md

Lines changed: 32 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
uid: how-to-check-object-types
33
title: How to Check Object Types
44
author: Morten Lønskov
5-
updated: 2026-04-09
5+
updated: 2026-04-10
66
applies_to:
77
products:
88
- product: Tabular Editor 2
@@ -12,44 +12,43 @@ applies_to:
1212
---
1313
# How to Check Object Types
1414

15-
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.
15+
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`.
1616

1717
## Quick reference
1818

1919
```csharp
20-
// Pattern matching (preferred)
20+
// Pattern matching -- checks type AND casts in one step
2121
if (col is CalculatedColumn cc)
22-
Info(cc.Expression);
22+
Info(cc.Expression); // Expression is only on CalculatedColumn, not base Column
2323
2424
// Filter a collection by type
2525
var calcCols = Model.AllColumns.OfType<CalculatedColumn>();
2626
var calcGroups = Model.Tables.OfType<CalculationGroupTable>();
2727

28-
// Runtime type name
28+
// Runtime type name (use only for display/logging, not for logic)
2929
string typeName = obj.GetType().Name; // "DataColumn", "Measure", etc.
30-
31-
// Null-safe cast
32-
var dc = col as DataColumn;
33-
if (dc != null) { /* use dc */ }
3430
```
3531

32+
> [!NOTE]
33+
> 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.
34+
3635
## Type hierarchy
3736

3837
The key inheritance relationships in the TOM wrapper:
3938

4039
| Base type | Subtypes |
4140
|---|---|
42-
| `Column` | `DataColumn`, `CalculatedColumn`, `CalculatedTableColumn` |
43-
| `Table` | `CalculatedTable`, `CalculationGroupTable` |
44-
| `Partition` | `MPartition`, `EntityPartition`, `PolicyRangePartition` |
45-
| `DataSource` | `ProviderDataSource`, `StructuredDataSource` |
41+
| (xref:TabularEditor.TOMWrapper.Column) | (xref:TabularEditor.TOMWrapper.DataColumn), (xref:TabularEditor.TOMWrapper.CalculatedColumn), (xref:TabularEditor.TOMWrapper.CalculatedTableColumn) |
42+
| (xref:TabularEditor.TOMWrapper.Table) | (xref:TabularEditor.TOMWrapper.CalculatedTable), (xref:TabularEditor.TOMWrapper.CalculationGroupTable) |
43+
| (xref:TabularEditor.TOMWrapper.Partition) | (xref:TabularEditor.TOMWrapper.MPartition), (xref:TabularEditor.TOMWrapper.EntityPartition), (xref:TabularEditor.TOMWrapper.PolicyRangePartition) |
44+
| (xref:TabularEditor.TOMWrapper.DataSource) | (xref:TabularEditor.TOMWrapper.ProviderDataSource), (xref:TabularEditor.TOMWrapper.StructuredDataSource) |
4645

4746
## Filtering collections by type
4847

49-
`OfType<T>()` filters and casts in one step. Prefer it over `Where(x => x is T)`.
48+
`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.
5049

5150
```csharp
52-
// All calculated columns in the model
51+
// All calculated columns in the model (empty if model has none)
5352
var calculatedColumns = Model.AllColumns.OfType<CalculatedColumn>();
5453

5554
// All M partitions (Power Query)
@@ -58,85 +57,51 @@ var mPartitions = Model.AllPartitions.OfType<MPartition>();
5857
// All calculation group tables
5958
var calcGroups = Model.Tables.OfType<CalculationGroupTable>();
6059

61-
// All regular tables (exclude calculation groups)
62-
var regularTables = Model.Tables.Where(t => t is not CalculationGroupTable);
60+
// All regular tables (exclude calculation groups and calculated tables)
61+
var regularTables = Model.Tables.Where(t => t is not CalculationGroupTable && t is not CalculatedTable);
6362
```
6463

6564
## Pattern matching with is
6665

67-
Use C# pattern matching to test and cast in a single expression.
68-
69-
```csharp
70-
foreach (var col in Model.AllColumns)
71-
{
72-
if (col is CalculatedColumn cc)
73-
Info($"{cc.Name}: {cc.Expression}");
74-
else if (col is DataColumn dc)
75-
Info($"{dc.Name}: data column in {dc.Table.Name}");
76-
}
77-
```
78-
79-
## Checking ObjectType enum
66+
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.
8067

81-
Every TOM object has an `ObjectType` property that returns an enum value. This identifies the base type, not the subtype.
68+
This is equivalent to:
8269

8370
```csharp
84-
foreach (var obj in Selected.Objects)
71+
if (col is CalculatedColumn)
8572
{
86-
switch (obj.ObjectType)
87-
{
88-
case ObjectType.Measure: /* ... */ break;
89-
case ObjectType.Column: /* ... */ break;
90-
case ObjectType.Table: /* ... */ break;
91-
case ObjectType.Hierarchy: /* ... */ break;
92-
}
73+
var cc = (CalculatedColumn)col; // explicit cast
74+
// use cc...
9375
}
9476
```
9577

96-
> [!WARNING]
97-
> `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.
98-
99-
## Checking partition source type
100-
101-
For partitions, use the `SourceType` property to distinguish between storage modes without type-casting.
78+
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`.
10279

10380
```csharp
104-
foreach (var p in Model.AllPartitions)
81+
foreach (var col in Model.AllColumns)
10582
{
106-
switch (p.SourceType)
107-
{
108-
case PartitionSourceType.M: /* Power Query */ break;
109-
case PartitionSourceType.Calculated: /* DAX calc table */ break;
110-
case PartitionSourceType.Entity: /* Direct Lake */ break;
111-
case PartitionSourceType.Query: /* Legacy SQL */ break;
112-
}
83+
// Expression is only available on CalculatedColumn, not the base Column type
84+
if (col is CalculatedColumn cc)
85+
Info($"{cc.Name}: {cc.Expression}");
86+
else if (col is DataColumn dc)
87+
Info($"{dc.Name}: data column in {dc.Table.Name}");
11388
}
11489
```
11590

11691
## Dynamic LINQ equivalent
11792

118-
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.
119-
120-
```
121-
// BPA expression context: the rule "Applies to" determines the object type
122-
// No C#-style type casting is available in Dynamic LINQ
123-
124-
// Check the object type name (rarely needed since scope handles this)
125-
ObjectTypeName = "Measure"
126-
ObjectTypeName = "Column"
127-
```
128-
129-
For subtypes like calculated columns, set the BPA rule scope to **Calculated Columns** rather than trying to filter by type in the expression.
93+
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.
13094

13195
## Common pitfalls
13296

13397
> [!IMPORTANT]
134-
> - `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`.
98+
> - `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`.
13599
> - `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.
136-
> - `ObjectType` and `SourceType` are base-level checks. For subtype-specific logic (e.g., accessing `Expression` on a `CalculatedColumn`), use `is` or `OfType<T>()`.
100+
> - Calculated table columns are managed automatically. Edit the calculated table's `Expression` to add or change columns. You cannot add them directly.
137101
138102
## See also
139103

140104
- @csharp-scripts
141105
- @using-bpa-sample-rules-expressions
142106
- @how-to-navigate-tom-hierarchy
107+
- @how-to-tom-interfaces

0 commit comments

Comments
 (0)