-
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
Open
mlonsk
wants to merge
5
commits into
main
Choose a base branch
from
user-mol/scripting-patterns-howtos
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
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,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"); | ||
mlonsk marked this conversation as resolved.
Show resolved
Hide 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"]; // 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 | ||
mlonsk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| ```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 | ||
mlonsk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| ```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 + ")", | ||
mlonsk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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 | ||
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,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 | ||
mlonsk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| ```csharp | ||
| // Pattern matching -- checks type AND casts in one step | ||
| if (col is CalculatedColumn cc) | ||
mlonsk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| Info(cc.Expression); // Expression is only on CalculatedColumn, not base Column | ||
|
|
||
| // Filter a collection by type | ||
| var calcCols = Model.AllColumns.OfType<CalculatedColumn>(); | ||
mlonsk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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. | ||
mlonsk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ``` | ||
|
|
||
| > [!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: | ||
mlonsk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| | 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 | ||
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.