-
Notifications
You must be signed in to change notification settings - Fork 16
Added How Tos for c# scripting. #307
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 2 commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
82ce09b
Added How tos around c# scripting.
mlonsk d80c8d1
Update with TE2 specific adaptations
mlonsk 5e301fe
updates based on gregs comments
mlonsk af07874
Add compiler link
mlonsk 7ed24a4
Greg recomendations round 2
mlonsk 879d888
Fixes to last few comments
mlonsk File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
|
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 | ||
|
mlonsk marked this conversation as resolved.
|
||
| // Add objects | ||
| table.AddMeasure("Name", "DAX Expression", "Display Folder"); | ||
|
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) | ||
|
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. | ||
|
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 | ||
|
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); | ||
|
mlonsk marked this conversation as resolved.
Outdated
|
||
| ``` | ||
|
|
||
| ## Adding hierarchies | ||
|
|
||
| Pass columns as parameters to automatically create levels. | ||
|
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 | ||
|
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. | ||
|
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; | ||
|
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 + ")", | ||
|
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. | ||
|
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. | ||
|
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 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. | ||
|
mlonsk marked this conversation as resolved.
Outdated
mlonsk marked this conversation as resolved.
Outdated
|
||
|
|
||
| ## Quick reference | ||
|
mlonsk marked this conversation as resolved.
|
||
|
|
||
| ```csharp | ||
| // Pattern matching (preferred) | ||
| if (col is CalculatedColumn cc) | ||
|
otykier marked this conversation as resolved.
|
||
| Info(cc.Expression); | ||
|
mlonsk marked this conversation as resolved.
Outdated
|
||
|
|
||
| // Filter a collection by type | ||
| var calcCols = Model.AllColumns.OfType<CalculatedColumn>(); | ||
|
mlonsk marked this conversation as resolved.
|
||
| var calcGroups = Model.Tables.OfType<CalculationGroupTable>(); | ||
|
|
||
| // Runtime type name | ||
| string typeName = obj.GetType().Name; // "DataColumn", "Measure", etc. | ||
|
mlonsk marked this conversation as resolved.
Outdated
|
||
|
|
||
| // Null-safe cast | ||
| var dc = col as DataColumn; | ||
|
mlonsk marked this conversation as resolved.
Outdated
|
||
| if (dc != null) { /* use dc */ } | ||
| ``` | ||
|
|
||
| ## Type hierarchy | ||
|
|
||
| The key inheritance relationships in the TOM wrapper: | ||
|
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] | ||
|
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. | ||
|
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`. | ||
|
mlonsk marked this conversation as resolved.
Outdated
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 | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.