Skip to content

Commit 82ce09b

Browse files
committed
Added How tos around c# scripting.
1 parent 9ca1dc2 commit 82ce09b

13 files changed

Lines changed: 1990 additions & 0 deletions

content/features/Useful-script-snippets.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ Here's a collection of small script snippets to get you started using the [Advan
2121

2222
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.
2323

24+
> [!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+
2427
***
2528

2629
## Create measures from columns
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
---
2+
uid: how-to-add-clone-remove-objects
3+
title: How to Add, Clone and Remove Objects
4+
author: Morten Lønskov
5+
updated: 2026-04-09
6+
applies_to:
7+
products:
8+
- product: Tabular Editor 2
9+
full: true
10+
- product: Tabular Editor 3
11+
full: true
12+
---
13+
# How to Add, Clone and Remove Objects
14+
15+
C# scripts can create new model objects, clone existing ones and delete objects. This article covers the Add, Clone and Delete patterns.
16+
17+
## Quick reference
18+
19+
```csharp
20+
// Add objects
21+
table.AddMeasure("Name", "DAX Expression", "Display Folder");
22+
table.AddCalculatedColumn("Name", "DAX Expression", "Display Folder");
23+
table.AddDataColumn("Name", "SourceColumn", "Display Folder", DataType.String);
24+
table.AddHierarchy("Name", "Display Folder", col1, col2, col3);
25+
Model.AddCalculatedTable("Name", "DAX Expression");
26+
Model.AddPerspective("Name");
27+
Model.AddRole("Name");
28+
Model.AddTranslation("da-DK");
29+
30+
// Relationships
31+
var rel = Model.AddRelationship();
32+
rel.FromColumn = Model.Tables["Sales"].Columns["ProductKey"];
33+
rel.ToColumn = Model.Tables["Product"].Columns["ProductKey"];
34+
35+
// Clone
36+
var clone = measure.Clone("New Name"); // same table
37+
var clone = measure.Clone("New Name", true, targetTable); // different table
38+
39+
// Delete (always materialize first when deleting in a loop)
40+
measure.Delete();
41+
table.Measures.Where(m => m.IsHidden).ToList().ForEach(m => m.Delete());
42+
```
43+
44+
## Adding measures
45+
46+
`AddMeasure()` creates a new measure on a table. All parameters except the first are optional.
47+
48+
```csharp
49+
var table = Model.Tables["Sales"];
50+
51+
// Simple measure
52+
var m = table.AddMeasure("Revenue", "SUM('Sales'[Amount])");
53+
m.FormatString = "#,##0.00";
54+
m.Description = "Total sales amount";
55+
56+
// With display folder
57+
var m2 = table.AddMeasure("Cost", "SUM('Sales'[Cost])", "Financial");
58+
```
59+
60+
## Adding columns
61+
62+
```csharp
63+
// Calculated column (DAX expression)
64+
var cc = table.AddCalculatedColumn("Profit", "'Sales'[Amount] - 'Sales'[Cost]");
65+
cc.DataType = DataType.Decimal;
66+
cc.FormatString = "#,##0.00";
67+
68+
// Data column (maps to a source column)
69+
var dc = table.AddDataColumn("Region", "RegionName", "Geography", DataType.String);
70+
```
71+
72+
## Adding hierarchies
73+
74+
Pass columns as parameters to automatically create levels.
75+
76+
```csharp
77+
var dateTable = Model.Tables["Date"];
78+
var h = dateTable.AddHierarchy(
79+
"Calendar",
80+
"",
81+
dateTable.Columns["Year"],
82+
dateTable.Columns["Quarter"],
83+
dateTable.Columns["Month"]
84+
);
85+
```
86+
87+
Or add levels one at a time:
88+
89+
```csharp
90+
var h = dateTable.AddHierarchy("Fiscal");
91+
h.AddLevel(dateTable.Columns["FiscalYear"]);
92+
h.AddLevel(dateTable.Columns["FiscalQuarter"]);
93+
h.AddLevel(dateTable.Columns["FiscalMonth"]);
94+
```
95+
96+
## Adding calculated tables
97+
98+
```csharp
99+
var ct = Model.AddCalculatedTable("DateKey List", "VALUES('Date'[DateKey])");
100+
```
101+
102+
## Adding relationships
103+
104+
`AddRelationship()` creates an empty relationship. You must set the columns explicitly.
105+
106+
```csharp
107+
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;
112+
```
113+
114+
## Cloning objects
115+
116+
`Clone()` creates a copy with all properties, annotations and translations.
117+
118+
```csharp
119+
// Clone within the same table
120+
var original = Model.AllMeasures.First(m => m.Name == "Revenue");
121+
var copy = original.Clone("Revenue Copy");
122+
123+
// Clone to a different table (with translations)
124+
var copy2 = original.Clone("Revenue Copy", true, Model.Tables["Reporting"]);
125+
```
126+
127+
## Generating measures from columns
128+
129+
A common pattern: iterate selected columns and create derived measures.
130+
131+
```csharp
132+
foreach (var col in Selected.Columns)
133+
{
134+
var m = col.Table.AddMeasure(
135+
"Sum of " + col.Name,
136+
"SUM(" + col.DaxObjectFullName + ")",
137+
col.DisplayFolder
138+
);
139+
m.FormatString = "0.00";
140+
col.IsHidden = true;
141+
}
142+
```
143+
144+
## Deleting objects
145+
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.
147+
148+
```csharp
149+
// Delete a single object
150+
Model.AllMeasures.First(m => m.Name == "Temp").Delete();
151+
152+
// Delete multiple objects safely
153+
Model.AllMeasures
154+
.Where(m => m.HasAnnotation("DEPRECATED"))
155+
.ToList()
156+
.ForEach(m => m.Delete());
157+
```
158+
159+
## Common pitfalls
160+
161+
> [!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.
166+
167+
## See also
168+
169+
- @useful-script-snippets
170+
- @script-create-sum-measures-from-columns
171+
- @how-to-navigate-tom-hierarchy
172+
- @how-to-use-selected-object
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
---
2+
uid: how-to-check-object-types
3+
title: How to Check Object Types
4+
author: Morten Lønskov
5+
updated: 2026-04-09
6+
applies_to:
7+
products:
8+
- product: Tabular Editor 2
9+
full: true
10+
- product: Tabular Editor 3
11+
full: true
12+
---
13+
# How to Check Object Types
14+
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.
16+
17+
## Quick reference
18+
19+
```csharp
20+
// Pattern matching (preferred)
21+
if (col is CalculatedColumn cc)
22+
Info(cc.Expression);
23+
24+
// Filter a collection by type
25+
var calcCols = Model.AllColumns.OfType<CalculatedColumn>();
26+
var calcGroups = Model.Tables.OfType<CalculationGroupTable>();
27+
28+
// Runtime type name
29+
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 */ }
34+
```
35+
36+
## Type hierarchy
37+
38+
The key inheritance relationships in the TOM wrapper:
39+
40+
| Base type | Subtypes |
41+
|---|---|
42+
| `Column` | `DataColumn`, `CalculatedColumn`, `CalculatedTableColumn` |
43+
| `Table` | `CalculatedTable`, `CalculationGroupTable` |
44+
| `Partition` | `MPartition`, `EntityPartition`, `PolicyRangePartition` |
45+
| `DataSource` | `ProviderDataSource`, `StructuredDataSource` |
46+
47+
## Filtering collections by type
48+
49+
`OfType<T>()` filters and casts in one step. Prefer it over `Where(x => x is T)`.
50+
51+
```csharp
52+
// All calculated columns in the model
53+
var calculatedColumns = Model.AllColumns.OfType<CalculatedColumn>();
54+
55+
// All M partitions (Power Query)
56+
var mPartitions = Model.AllPartitions.OfType<MPartition>();
57+
58+
// All calculation group tables
59+
var calcGroups = Model.Tables.OfType<CalculationGroupTable>();
60+
61+
// All regular tables (exclude calculation groups)
62+
var regularTables = Model.Tables.Where(t => t is not CalculationGroupTable);
63+
```
64+
65+
## Pattern matching with is
66+
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
80+
81+
Every TOM object has an `ObjectType` property that returns an enum value. This identifies the base type, not the subtype.
82+
83+
```csharp
84+
foreach (var obj in Selected.Objects)
85+
{
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+
}
93+
}
94+
```
95+
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.
102+
103+
```csharp
104+
foreach (var p in Model.AllPartitions)
105+
{
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+
}
113+
}
114+
```
115+
116+
## Dynamic LINQ equivalent
117+
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.
130+
131+
## Common pitfalls
132+
133+
> [!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`.
135+
> - `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>()`.
137+
138+
## See also
139+
140+
- @csharp-scripts
141+
- @using-bpa-sample-rules-expressions
142+
- @how-to-navigate-tom-hierarchy

0 commit comments

Comments
 (0)