
C Sharp Scripting
- 35 installs
- 836 repo stars
- Updated July 29, 2026
- data-goblin/power-bi-agentic-development
Write C# scripts against the Tabular Editor object model to bulk-edit Power BI semantic models, such as adding descriptions to all tables, columns, and measures.
About
Provides C# scripting patterns against the Tabular Editor Model object model to programmatically iterate and modify tables, columns, and measures in a semantic model. A developer uses it to automate bulk model edits like setting default descriptions across all objects.
- Iterates Model.Tables and Model.AllMeasures to edit objects
- Bulk-sets descriptions on tables, columns, and measures
C Sharp Scripting by the numbers
- 35 all-time installs (skills.sh)
- Ranked #1,060 of 2,064 Data Science & ML skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/data-goblin/power-bi-agentic-development --skill c-sharp-scriptingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 35 |
|---|---|
| repo stars | ★ 836 |
| Last updated | July 29, 2026 |
| Repository | data-goblin/power-bi-agentic-development ↗ |
What it does
Write C# scripts against the Tabular Editor object model to bulk-edit Power BI semantic models, such as adding descriptions to all tables, columns, and measures.
Files
// Example: Add Descriptions to All Objects
// This script adds default descriptions to all tables, columns, and measures
// Add descriptions to tables
foreach(var table in Model.Tables) {
if(string.IsNullOrEmpty(table.Description)) {
table.Description = "Table: " + table.Name;
}
}
// Add descriptions to columns
foreach(var table in Model.Tables) {
foreach(var column in table.Columns) {
if(string.IsNullOrEmpty(column.Description)) {
column.Description = column.Name + " from " + table.Name;
}
}
}
// Add descriptions to measures
foreach(var measure in Model.AllMeasures) {
if(string.IsNullOrEmpty(measure.Description)) {
measure.Description = "Measure: " + measure.Name;
}
}
Info("Added descriptions to all objects");
/*
* Title: Add expression to measure descriptions
*
* Author: Mihaly Kavasi, Ed Hansberry
*
* Description: Adds the DAX expression to the description of every measure
* in the model. If a description exists, appends the expression.
*
* Usage: Run this script on the entire model.
* CLI: te "workspace/model" script.csx --file
*
* Non-interactive: Yes (works on Model.AllMeasures)
*/
var updatedCount = 0;
foreach(var m in Model.AllMeasures)
{
if(m.Description == "")
{
m.Description = "Expression:" + "\n" + m.Expression;
updatedCount++;
}
else if (!m.Description.Contains("Expression"))
{
m.Description = m.Description + "\n" + "Expression:" + "\n" + m.Expression;
updatedCount++;
}
else
{
// Reset expressions already added
int pos = m.Description.IndexOf("Expression",0);
bool onlyExpression = (pos == 0);
if (onlyExpression) {
m.Description = "Expression:" + "\n" + m.Expression;
} else {
m.Description = m.Description.Substring(0,pos-1) + "\n" + "Expression:" + "\n" + m.Expression;
}
updatedCount++;
}
}
// Format DAX for better readability
Model.AllMeasures.FormatDax();
Info("Updated descriptions for " + updatedCount + " measures");
/*
* Title: Clean object names (CamelCase to Proper Case)
*
* Author: Darren Gosbell, twitter.com/DarrenGosbell
*
* Description: Converts CamelCaseNames to Proper Case Names by inserting
* spaces before uppercase characters. Ignores objects with spaces already.
* before: CalendarYearNum
* after: Calendar Year Num
*
* Usage: Run this script on the entire model.
* CLI: te "workspace/model" script.csx --file
*
* Non-interactive: Yes (works on Model.Tables)
*/
// Regular expression splits on underscores and case changes
var rex = new System.Text.RegularExpressions.Regex( "(^[a-z]+|[A-Z]+(?![a-z])|[A-Z][a-z]+|[^A-Z,a-z]+|[_]|[a-z]+)");
// Table prefixes to strip (e.g., "dim", "fact", "vw")
List<string> tablePrefixesToIgnore = new List<string>() {"dim","fact", "vw","tbl","vd","td","tf","vf"};
// Table suffixes to strip (e.g., "dim", "fact")
List<string> tableSuffixesToIgnore = new List<string>() {"dim", "fact"};
var renamedTables = 0;
var renamedColumns = 0;
foreach (var tbl in Model.Tables)
{
if (!tbl.IsHidden && !tbl.Name.Contains(" "))
{
string name = tbl.Name;
var matches = rex.Matches(name);
var firstWord = matches[0];
var lastWord = matches[matches.Count-1];
string[] words = matches
.OfType<System.Text.RegularExpressions.Match>()
.Where(m =>
m.Value != "_"
&& !(m == firstWord && tablePrefixesToIgnore.Contains(m.Value,System.StringComparer.OrdinalIgnoreCase))
&& !(m == lastWord && tableSuffixesToIgnore.Contains(m.Value,System.StringComparer.OrdinalIgnoreCase ))
)
.Select(m => char.ToUpper(m.Value.First()) + m.Value.Substring(1))
.ToArray();
string result = string.Join(" ", words);
tbl.Name = result;
renamedTables++;
}
foreach (var col in tbl.Columns)
{
if (!col.IsHidden && !col.Name.Contains(" "))
{
string name = col.Name;
string[] words = rex.Matches(name)
.OfType<System.Text.RegularExpressions.Match>()
.Where(m => m.Value != "_" )
.Select(m => char.ToUpper(m.Value.First()) + m.Value.Substring(1))
.ToArray();
string result = string.Join(" ", words);
col.Name = result;
renamedColumns++;
}
}
}
Info("Renamed " + renamedTables + " tables and " + renamedColumns + " columns");
// Example: Create Measures from Column List
// This script creates SUM measures for a list of columns
var table = Model.Tables["Sales"];
var columns = new[] { "Amount", "Quantity", "Discount", "Tax", "Freight" };
foreach(var columnName in columns) {
if(table.Columns.Contains(columnName)) {
var measure = table.AddMeasure("Total " + columnName, "SUM(Sales[" + columnName + "])");
measure.FormatString = "$#,0";
measure.DisplayFolder = "Totals";
}
}
Info("Created measures for " + columns.Length + " columns");
// Example: Initialize New Model
// This script performs common initialization tasks: hide keys, disable summarization, create base measures
// Step 1: Hide all key columns
var hiddenCount = 0;
foreach(var table in Model.Tables) {
foreach(var column in table.Columns) {
if(column.Name.Contains("Key") || column.Name.EndsWith("ID")) {
column.IsHidden = true;
hiddenCount++;
}
// Disable summarization for all columns
column.SummarizeBy = AggregateFunction.None;
}
}
Info("Step 1: Hidden " + hiddenCount + " key columns and disabled summarization");
// Step 2: Create base measures for fact tables
var sales = Model.Tables["Sales"];
var m1 = sales.AddMeasure("Total Sales", "SUM(Sales[Amount])");
m1.FormatString = "$#,0";
m1.DisplayFolder = "Base Measures";
var m2 = sales.AddMeasure("Total Quantity", "SUM(Sales[Quantity])");
m2.FormatString = "#,0";
m2.DisplayFolder = "Base Measures";
var m3 = sales.AddMeasure("Sales Count", "COUNTROWS(Sales)");
m3.FormatString = "#,0";
m3.DisplayFolder = "Base Measures";
Info("Step 2: Created 3 base measures in Sales table");
// Step 3: Create calculated measures
var m4 = sales.AddMeasure("Average Sale Amount");
m4.Expression = "DIVIDE([Total Sales], [Sales Count])";
m4.FormatString = "$#,0.00";
m4.DisplayFolder = "Calculated Measures";
Info("Step 3: Created calculated measures");
Info("Model initialization complete!");
Bulk Operations Scripts
Scripts for performing batch operations across multiple model objects.
Available Scripts
add-format-strings.csx- Interactive dialog to apply custom format strings to selected measuresadd-measure-selection.csx- Bulk add measures based on selection criteriaadd-time-intelligence.csx- Bulk create time intelligence measuresadd_descriptions_to_all.csx- Add descriptions to all model objectsadd_expression_to_descriptions.csx- Append DAX expressions to measure descriptionsclean_object_names.csx- Convert CamelCase names to Proper Case with spacescreate_measures_from_columns.csx- Generate measures from numeric columnsinitialize_model.csx- Initialize new model with common setup taskssync_folders_from_names.csx- Sync display folders based on naming patternsupdate_descriptions_from_comments.csx- Extract DAX comments to descriptionsvalidate_and_fix_issues.csx- Validate model and fix common issues
Usage Examples
Execute Inline
te "model.bim" 'foreach(var m in Model.AllMeasures) { m.Description = m.Expression; }'Execute Script File
te "model.bim" samples/bulk-operations/clean_object_names.csx --file
te "Production/Sales" samples/bulk-operations/initialize_model.csx --fileWith Fabric CLI Workflow
# Export model
fab export "Workspace/Model.SemanticModel" -o ./model -f
# Run bulk operations
te "./model/Model.SemanticModel/model.bim" samples/bulk-operations/clean_object_names.csx --file
te "./model/Model.SemanticModel/model.bim" samples/bulk-operations/add_descriptions_to_all.csx --file
# Import back
fab import "Workspace/Model.SemanticModel" -i ./model/Model.SemanticModel -fCommon Patterns
Clean All Object Names
#r "System.Text.RegularExpressions"
var rex = new System.Text.RegularExpressions.Regex("(^[a-z]+|[A-Z]+(?![a-z])|[A-Z][a-z]+)");
foreach (var tbl in Model.Tables) {
if (!tbl.Name.Contains(" ")) {
var words = rex.Matches(tbl.Name)
.OfType<System.Text.RegularExpressions.Match>()
.Select(m => char.ToUpper(m.Value.First()) + m.Value.Substring(1))
.ToArray();
tbl.Name = string.Join(" ", words);
}
}Initialize Model Setup
// Hide key columns and disable summarization
foreach(var table in Model.Tables) {
foreach(var column in table.Columns) {
if(column.Name.Contains("Key") || column.Name.EndsWith("ID")) {
column.IsHidden = true;
}
column.SummarizeBy = AggregateFunction.None;
}
}Add Descriptions to All Objects
// Add descriptions to measures
foreach(var measure in Model.AllMeasures) {
if(string.IsNullOrEmpty(measure.Description)) {
measure.Description = "Measure: " + measure.Name;
}
}
// Add descriptions to columns
foreach(var column in Model.AllColumns) {
if(string.IsNullOrEmpty(column.Description)) {
column.Description = "Column: " + column.Name;
}
}Sync Display Folders from Names
// Extract folder from naming pattern (e.g., "Sales - Revenue" -> "Sales")
foreach(var measure in Model.AllMeasures) {
if(measure.Name.Contains(" - ")) {
var parts = measure.Name.Split(new[] { " - " }, 2, StringSplitOptions.None);
measure.DisplayFolder = parts[0];
}
}Property Reference
Common Properties for Bulk Operations
Model.AllMeasures- All measures in the modelModel.AllColumns- All columns in the modelModel.AllTables- All tables in the modelSelected.Measures- Currently selected measuresSelected.Columns- Currently selected columnsSelected.Tables- Currently selected tables
Object Properties
Name- Object nameDescription- Object descriptionDisplayFolder- Display folder pathIsHidden- Visibility flagExpression- DAX expression (measures, calculated columns)FormatString- Number format
Best Practices
1. Backup First
- Always save a backup before bulk operations
- Test on a copy of the model first
- Use version control for model files
2. Use Filters
- Filter objects before bulk operations
- Use LINQ Where() clauses to target specific objects
- Validate selection before making changes
3. Logging
- Use Info() to report what was changed
- Count affected objects and display summary
- Log errors with Error() for troubleshooting
4. Performance
- Process large models in batches if needed
- Disable refresh during bulk operations
- Use Selected. when possible instead of Model.All
See Also
- Measures
- Columns
- Display Folders
- Format Strings
// Example: Sync Display Folders from Measure Names
// This script organizes measures by prefix (e.g., "Sales Total" -> "Sales" folder)
foreach(var measure in Model.AllMeasures) {
var parts = measure.Name.Split(new[] { ' ' }, 2);
if(parts.Length > 1) {
measure.DisplayFolder = parts[0]; // First word becomes folder
}
}
Info("Synced display folders from measure names");
// Example: Update All Measure Descriptions from Comments
// This script extracts the first comment line from measure expressions and uses it as the description
var updatedCount = 0;
foreach(var measure in Model.AllMeasures) {
// Extract first line of expression
var lines = measure.Expression.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
if(lines.Length > 0) {
var firstLine = lines[0].Trim();
// If first line is a comment, use it as description
if(firstLine.StartsWith("//")) {
measure.Description = firstLine.Substring(2).Trim();
updatedCount++;
}
}
}
Info("Updated descriptions for " + updatedCount + " measures");
// Example: Validate and Fix Common Issues
// This script checks for and fixes common model issues
var issues = 0;
// Check for measures without format strings
foreach(var measure in Model.AllMeasures) {
if(string.IsNullOrEmpty(measure.FormatString)) {
measure.FormatString = "#,0"; // Default format
issues++;
}
}
// Check for hidden columns that should be keys
foreach(var table in Model.Tables) {
foreach(var column in table.Columns) {
if(!column.IsHidden && (column.Name.EndsWith("Key") || column.Name.EndsWith("ID"))) {
column.IsHidden = true;
issues++;
}
}
}
Info("Fixed " + issues + " issues");
// Example: Currency Conversion Calculation Group
// This script creates a calculation group for currency conversion
var cg = Model.AddCalculationGroup("Currency");
// Note: Precedence property only exists in TE3, not TE2
// USD (base)
var usd = cg.AddCalculationItem("USD");
usd.Expression = "SELECTEDMEASURE()";
// EUR
var eur = cg.AddCalculationItem("EUR");
eur.Expression = "SELECTEDMEASURE() * 0.85"; // Example rate
// GBP
var gbp = cg.AddCalculationItem("GBP");
gbp.Expression = "SELECTEDMEASURE() * 0.73"; // Example rate
Info("Created Currency calculation group");
Calculation Groups Scripts
Scripts for creating and managing calculation groups in Tabular models.
Available Scripts
time_intelligence.csx- Create time intelligence calculation group (YTD, MTD, QTD, YoY, etc.)currency_conversion.csx- Create currency conversion calculation group
Usage Examples
Execute Script File
te "model.bim" samples/calculation-groups/time_intelligence.csx --file
te "Production/Sales" samples/calculation-groups/currency_conversion.csx --fileExecute Inline
te "model.bim" 'var cg = Model.AddCalculationGroup("Time Intelligence"); cg.AddCalculationItem("YTD", "CALCULATE(SELECTEDMEASURE(), DATESYTD(Date[Date]))");'With Fabric CLI Workflow
# Export model
fab export "Workspace/Model.SemanticModel" -o ./model -f
# Create calculation groups
te "./model/Model.SemanticModel/model.bim" samples/calculation-groups/time_intelligence.csx --file
# Import back
fab import "Workspace/Model.SemanticModel" -i ./model/Model.SemanticModel -fCommon Patterns
Create Calculation Group with Items
// Create calculation group
var cg = Model.AddCalculationGroup("Time Intelligence");
cg.Precedence = 10;
cg.Description = "Time intelligence calculations";
// Add calculation items
var ytd = cg.AddCalculationItem("YTD");
ytd.Expression = "CALCULATE(SELECTEDMEASURE(), DATESYTD('Date'[Date]))";
ytd.Ordinal = 0;
var mtd = cg.AddCalculationItem("MTD");
mtd.Expression = "CALCULATE(SELECTEDMEASURE(), DATESMTD('Date'[Date]))";
mtd.Ordinal = 1;Year-over-Year Calculation
var yoy = cg.AddCalculationItem("YoY %");
yoy.Expression = @"
VAR CurrentValue = SELECTEDMEASURE()
VAR PriorValue = CALCULATE(SELECTEDMEASURE(), SAMEPERIODLASTYEAR('Date'[Date]))
RETURN
DIVIDE(CurrentValue - PriorValue, PriorValue)
";
yoy.FormatString = "0.0%";
yoy.Ordinal = 5;Currency Conversion
var cg = Model.AddCalculationGroup("Currency");
cg.Precedence = 20;
var usd = cg.AddCalculationItem("USD");
usd.Expression = "SELECTEDMEASURE()";
var eur = cg.AddCalculationItem("EUR");
eur.Expression = "SELECTEDMEASURE() * 0.85";Set Precedence
// Lower precedence evaluates first
foreach(var cg in Model.CalculationGroups) {
if(cg.Name == "Time Intelligence") {
cg.Precedence = 10;
}
else if(cg.Name == "Currency") {
cg.Precedence = 20;
}
}Property Reference
CalculationGroup Properties
Name- Calculation group nameDescription- DescriptionPrecedence- Evaluation order (lower = first)CalculationItems- Collection of calculation itemsAddCalculationItem(name, expression)- Add new calculation item
CalculationItem Properties
Name- Item nameExpression- DAX expression using SELECTEDMEASURE()FormatString- Optional format overrideOrdinal- Display orderDescription- Item description
Best Practices
1. Precedence
- Use lower precedence for time intelligence (e.g., 10)
- Use higher precedence for formatting/currency (e.g., 20)
- Leave gaps (10, 20, 30) for future additions
2. SELECTEDMEASURE()
- Always use SELECTEDMEASURE() to reference the base measure
- Use variables for clarity in complex calculations
- Test with different base measures
3. Format Strings
- Override format for percentage calculations
- Maintain base measure format when not specified
- Use FormatString property on calculation items
4. Naming
- Use clear, concise names (YTD, MTD, PY)
- Use consistent naming across calculation groups
- Document complex calculations in descriptions
See Also
- Measures
- Format Strings
- Tables
// Example: Create Time Intelligence Calculation Group
// This script creates a calculation group with common time intelligence patterns
Info("Creating Time Intelligence calculation group...");
// Create calculation group
var cg = Model.AddCalculationGroup("Time Intelligence");
// Note: Precedence property only exists in TE3, not TE2
cg.Description = "Time intelligence calculations for all measures";
Info("Created calculation group");
// Current Period (default)
var current = cg.AddCalculationItem("Current", "SELECTEDMEASURE()");
current.Ordinal = 0;
// Year-to-Date
var ytd = cg.AddCalculationItem("YTD");
ytd.Expression = "CALCULATE(SELECTEDMEASURE(), DATESYTD('Date'[Date]))";
ytd.Ordinal = 1;
// Month-to-Date
var mtd = cg.AddCalculationItem("MTD");
mtd.Expression = "CALCULATE(SELECTEDMEASURE(), DATESMTD('Date'[Date]))";
mtd.Ordinal = 2;
// Quarter-to-Date
var qtd = cg.AddCalculationItem("QTD");
qtd.Expression = "CALCULATE(SELECTEDMEASURE(), DATESQTD('Date'[Date]))";
qtd.Ordinal = 3;
// Prior Year
var py = cg.AddCalculationItem("PY");
py.Expression = "CALCULATE(SELECTEDMEASURE(), SAMEPERIODLASTYEAR('Date'[Date]))";
py.Ordinal = 4;
// Year-over-Year Growth %
var yoy = cg.AddCalculationItem("YoY %");
yoy.Expression = @"
VAR CurrentValue = SELECTEDMEASURE()
VAR PriorValue = CALCULATE(SELECTEDMEASURE(), SAMEPERIODLASTYEAR('Date'[Date]))
RETURN
DIVIDE(CurrentValue - PriorValue, PriorValue)
";
yoy.Ordinal = 5;
// Month-over-Month Growth %
var mom = cg.AddCalculationItem("MoM %");
mom.Expression = @"
VAR CurrentValue = SELECTEDMEASURE()
VAR PriorValue = CALCULATE(SELECTEDMEASURE(), DATEADD('Date'[Date], -1, MONTH))
RETURN
DIVIDE(CurrentValue - PriorValue, PriorValue)
";
mom.Ordinal = 6;
// Prior Month
var pm = cg.AddCalculationItem("PM");
pm.Expression = "CALCULATE(SELECTEDMEASURE(), DATEADD('Date'[Date], -1, MONTH))";
pm.Ordinal = 7;
// Rolling 12 Months
var r12m = cg.AddCalculationItem("R12M");
r12m.Expression = @"
CALCULATE(
SELECTEDMEASURE(),
DATESINPERIOD('Date'[Date], MAX('Date'[Date]), -12, MONTH)
)
";
r12m.Ordinal = 8;
Info("Created 9 time intelligence calculation items");
Info("Time Intelligence calculation group complete!");
// Example: Add Calculated Column
// This script adds a calculated column to a table
var column = Model.Tables["Sales"].AddCalculatedColumn("Profit");
column.Expression = "Sales[Revenue] - Sales[Cost]";
column.FormatString = "$#,0";
column.Description = "Calculated profit";
Info("Added calculated column: " + column.Name);
/*
* Title: Add Calculated Column
*
* Description: Creates calculated columns with DAX expressions that evaluate
* at refresh time with row context.
*
* WHEN TO USE CALCULATED COLUMNS:
* - Row-by-row calculations (Profit = Revenue - Cost)
* - Categories/segmentation for slicing (Age bands, customer segments)
* - Composite keys or derived keys for relationships
* - Row-level business rules (Status based on dates)
* - When you need to FILTER on the result
*
* WHEN TO USE MEASURES INSTEAD:
* - Aggregations (SUM, AVERAGE, COUNT)
* - Dynamic calculations based on filters
* - Time intelligence (YTD, PY, MoM)
* - To minimize model size (measures don't use memory)
*
* KEY DIFFERENCE:
* Calculated Columns: ROW CONTEXT (can reference [Column] directly)
* Measures: FILTER CONTEXT (must use SUM([Column]), AVERAGE([Column]), etc.)
*
* Usage: Configure calculated columns below.
* CLI: te "workspace/model" add-calculated-column.csx --file
*
* Non-interactive: Yes
*/
// ============================================================================
// CONFIGURATION
// ============================================================================
var tableName = "Sales";
// ============================================================================
// EXAMPLE 1: Simple Row-Level Calculation
// ============================================================================
var profitCol = Model.Tables[tableName].AddCalculatedColumn("Profit");
profitCol.Expression = "[Revenue] - [Cost]";
profitCol.FormatString = "$#,##0.00";
profitCol.Description = "Profit calculated as Revenue minus Cost";
profitCol.DisplayFolder = "Calculations";
profitCol.DataType = DataType.Decimal;
Info("✓ Added: Profit (row-level calculation)");
// ============================================================================
// EXAMPLE 2: Percentage Calculation
// ============================================================================
var marginCol = Model.Tables[tableName].AddCalculatedColumn("Margin %");
marginCol.Expression = "DIVIDE([Revenue] - [Cost], [Revenue])";
marginCol.FormatString = "0.00%";
marginCol.Description = "Profit margin percentage";
marginCol.DisplayFolder = "Calculations";
marginCol.DataType = DataType.Double;
Info("✓ Added: Margin % (percentage calculation)");
// ============================================================================
// EXAMPLE 3: Category/Segmentation Column
// ============================================================================
var revenueSegmentCol = Model.Tables[tableName].AddCalculatedColumn("Revenue Segment");
revenueSegmentCol.Expression = @"
SWITCH(
TRUE(),
[Revenue] < 100, ""Small"",
[Revenue] < 1000, ""Medium"",
[Revenue] < 10000, ""Large"",
""Enterprise""
)";
revenueSegmentCol.DataType = DataType.String;
revenueSegmentCol.Description = "Revenue-based customer segmentation";
revenueSegmentCol.DisplayFolder = "Segments";
revenueSegmentCol.SummarizeBy = AggregateFunction.None;
Info("✓ Added: Revenue Segment (for slicing/filtering)");
// ============================================================================
// EXAMPLE 4: Conditional Logic Column
// ============================================================================
var statusCol = Model.Tables[tableName].AddCalculatedColumn("Order Status");
statusCol.Expression = @"
IF(
ISBLANK([ShipDate]),
""Pending"",
IF(
[ShipDate] > [OrderDate] + 7,
""Delayed"",
""On Time""
)
)";
statusCol.DataType = DataType.String;
statusCol.Description = "Order status based on ship date";
statusCol.DisplayFolder = "Attributes";
statusCol.SummarizeBy = AggregateFunction.None;
Info("✓ Added: Order Status (conditional logic)");
// ============================================================================
// EXAMPLE 5: Composite Key Column
// ============================================================================
var compositeKeyCol = Model.Tables[tableName].AddCalculatedColumn("CustomerProductKey");
compositeKeyCol.Expression = "[CustomerKey] & \"-\" & [ProductKey]";
compositeKeyCol.DataType = DataType.String;
compositeKeyCol.Description = "Composite key for Customer-Product relationships";
compositeKeyCol.IsHidden = true;
compositeKeyCol.SummarizeBy = AggregateFunction.None;
Info("✓ Added: CustomerProductKey (composite key)");
// ============================================================================
// EXAMPLE 6: Date-Based Calculation
// ============================================================================
var daysToShipCol = Model.Tables[tableName].AddCalculatedColumn("Days to Ship");
daysToShipCol.Expression = "DATEDIFF([OrderDate], [ShipDate], DAY)";
daysToShipCol.FormatString = "#,0";
daysToShipCol.DataType = DataType.Int64;
daysToShipCol.Description = "Number of days between order and shipment";
daysToShipCol.DisplayFolder = "Metrics";
Info("✓ Added: Days to Ship (date calculation)");
// ============================================================================
// SUMMARY
// ============================================================================
Info("\nAdded 6 calculated columns to " + tableName + ":");
Info(" 1. Profit - Simple arithmetic");
Info(" 2. Margin % - Division with DIVIDE");
Info(" 3. Revenue Segment - SWITCH for categorization");
Info(" 4. Order Status - Nested IF conditional logic");
Info(" 5. CustomerProductKey - String concatenation for composite key");
Info(" 6. Days to Ship - DATEDIFF for date math");
Info("\nREMEMBER:");
Info(" - Calculated columns use ROW CONTEXT");
Info(" - Reference columns directly: [Revenue], not SUM([Revenue])");
Info(" - Evaluated at REFRESH time (stored in model)");
Info(" - Use for slicing/filtering, not aggregation");
Info(" - Increase model size based on cardinality");
/*
* Title: Disable column summarization
*
* Author: Tabular Editor Community
*
* Description: Disables automatic summarization for all columns in the model.
* Useful when you want to force users to use explicit measures only.
*
* Usage: Run this script on the entire model.
* CLI: te "workspace/model" script.csx --file
*
* Non-interactive: Yes (works on Model.Tables)
*/
var updatedCount = 0;
foreach(var table in Model.Tables) {
foreach(var column in table.Columns) {
column.SummarizeBy = AggregateFunction.None;
updatedCount++;
}
}
Info("Disabled summarization on " + updatedCount + " columns");
/*
* Title: Disable Available In MDX
*
* Description: Sets IsAvailableInMDX to false for specified columns.
* This excludes columns from MDX query tools (Excel PivotTables, SSRS, etc.)
* while keeping them available in DAX. Useful for hiding technical columns
* from legacy MDX tools.
*
* Common use cases:
* - Sort columns (MonthNumber, WeekdayNumber)
* - Technical/system columns
* - Helper columns not meant for end users
*
* Usage: Configure target columns below.
* CLI: te "workspace/model" disable-available-in-mdx.csx --file
*
* Non-interactive: Yes
*/
// ============================================================================
// CONFIGURATION
// ============================================================================
// Option 1: Specify table and column names
var specificColumns = new Dictionary<string, List<string>>
{
{ "Date", new List<string> {
"MonthNumber",
"WeekdayNumber",
"YearNumber",
"QuarterNumber",
"WeekNumber"
}},
{ "Sales", new List<string> {
"RowNumber",
"SourceSystemID"
}}
};
// Option 2: Pattern-based (uncomment to use)
var usePatternBased = false;
var patternBasedTables = new[] { "Date", "Sales", "Customers" };
// ============================================================================
// SCRIPT LOGIC - OPTION 1: SPECIFIC COLUMNS
// ============================================================================
var updatedCount = 0;
if(!usePatternBased)
{
foreach(var tableEntry in specificColumns)
{
var tableName = tableEntry.Key;
var columnNames = tableEntry.Value;
if(!Model.Tables.Contains(tableName))
{
Info("⚠ Table not found: " + tableName);
continue;
}
var table = Model.Tables[tableName];
foreach(var columnName in columnNames)
{
if(table.Columns.Contains(columnName))
{
table.Columns[columnName].IsAvailableInMDX = false;
updatedCount++;
Info("✓ Disabled MDX: " + tableName + "[" + columnName + "]");
}
else
{
Info("⚠ Column not found: " + tableName + "[" + columnName + "]");
}
}
}
}
// ============================================================================
// SCRIPT LOGIC - OPTION 2: PATTERN-BASED
// ============================================================================
else
{
foreach(var tableName in patternBasedTables)
{
if(!Model.Tables.Contains(tableName))
{
Info("⚠ Table not found: " + tableName);
continue;
}
var table = Model.Tables[tableName];
foreach(var column in table.Columns)
{
// Disable MDX for hidden columns
if(column.IsHidden)
{
column.IsAvailableInMDX = false;
updatedCount++;
}
// Disable MDX for columns ending in "Number" (sort columns)
else if(column.Name.EndsWith("Number") && column.DataType == DataType.Int64)
{
column.IsAvailableInMDX = false;
updatedCount++;
}
// Disable MDX for columns ending in "ID" or "Key" (technical columns)
else if(column.Name.EndsWith("ID") || column.Name.EndsWith("Key"))
{
column.IsAvailableInMDX = false;
updatedCount++;
}
// Disable MDX for columns starting with "_" (system columns)
else if(column.Name.StartsWith("_"))
{
column.IsAvailableInMDX = false;
updatedCount++;
}
}
}
}
Info("\nDisabled IsAvailableInMDX for " + updatedCount + " columns");
// ============================================================================
// NOTES
// ============================================================================
Info("\nNOTE:");
Info("- IsAvailableInMDX = false hides columns from Excel PivotTables and MDX tools");
Info("- Columns remain available in DAX and Power BI");
Info("- Typically used for sort columns and technical fields");
/*
* Title: Get Column Cardinality
*
* Author: Tabular Editor Community
*
* Description: Retrieves cardinality (distinct value count) for columns.
* High cardinality columns can impact model size and query performance.
*
* Usage: Run on selected columns or all columns in model
* CLI: te "workspace/model" samples/columns/get-column-cardinality.csx --file
*
* Options:
* - Set useSelection = true to analyze only selected columns
* - Set topN to control how many high-cardinality columns to show
*
* Non-interactive: Works on Selected.Columns or Model.AllColumns
*/
bool useSelection = false; // Set to true to analyze only selected columns
int topN = 20; // Number of top columns to display
var columnsToAnalyze = useSelection ? Selected.Columns : Model.AllColumns;
if (!columnsToAnalyze.Any()) {
Error("No columns to analyze. Select columns or set useSelection = false");
}
else {
Info("Analyzing cardinality for " + columnsToAnalyze.Count() + " columns...");
var cardinalityStats = new List<Tuple<string, string, long, string>>();
foreach(var column in columnsToAnalyze.Where(c => c.Type != ColumnType.RowNumber)) {
try {
string dax = "COUNTROWS(DISTINCT(" + column.DaxObjectFullName + "))";
dynamic result = EvaluateDax(dax);
long cardinality = 0;
if (result != null && long.TryParse(result.ToString(), out cardinality)) {
cardinalityStats.Add(Tuple.Create(
column.Table.Name,
column.Name,
cardinality,
column.DataType.ToString()
));
}
} catch (Exception ex) {
Error("Failed to get cardinality for [" + column.Table.Name + "].[" + column.Name + "]: " + ex.Message);
}
}
// Sort by cardinality (highest first)
var sortedByCardinality = cardinalityStats.OrderByDescending(c => c.Item3).ToList();
// Output results
Info("");
Info("=== COLUMN CARDINALITY ANALYSIS ===");
Info("Analyzed Columns: " + cardinalityStats.Count);
Info("");
Info("=== TOP " + topN + " COLUMNS BY CARDINALITY ===");
Info("Table.Column | Cardinality | Data Type");
Info("".PadRight(70, '-'));
foreach(var column in sortedByCardinality.Take(topN)) {
string fullName = "[" + column.Item1 + "].[" + column.Item2 + "]";
Info(fullName.PadRight(40) + " | " + column.Item3.ToString("N0").PadLeft(12) + " | " + column.Item4);
}
}
/*
* Title: Get Column Sizes
*
* Author: Tabular Editor Community
*
* Description: Retrieves VertiPaq storage statistics for all columns,
* showing which columns consume the most memory. Useful for optimization.
*
* Usage: Run against connected model to get column size analysis
* CLI: te "workspace/model" samples/columns/get-column-sizes.csx --file
*
* Options: Modify topN variable to show more/fewer columns
*
* Non-interactive: Yes (works on Model object)
*/
int topN = 20;
var query = @"
TOPN(
" + topN + @",
SELECTCOLUMNS(
INFO.STORAGETABLECOLUMNS(),
""Table"", [DIMENSION_NAME],
""Column"", [ATTRIBUTE_NAME],
""Size MB"", [DICTIONARY_SIZE] / 1024 / 1024
),
[Size MB],
DESC
)";
try {
dynamic result = EvaluateDax(query);
Info("=== COLUMN SIZE ANALYSIS ===");
Info("Total Columns Analyzed: " + result.Rows.Count);
Info("");
Info("=== TOP " + topN + " COLUMNS BY SIZE ===");
Info("Table.Column | Size");
Info("".PadRight(70, '-'));
for (int i = 0; i < result.Rows.Count; i++) {
var table = result.Rows[i][0];
var column = result.Rows[i][1];
var sizeMB = Convert.ToDouble(result.Rows[i][2]).ToString("N2");
string fullName = "[" + table + "].[" + column + "]";
Info(fullName.PadRight(50) + " | " + sizeMB + " MB");
}
} catch (Exception ex) {
Error("Failed to get column sizes: " + ex.Message);
}
/*
* Title: Hide key/ID columns
*
* Author: Tabular Editor Community
*
* Description: Hides all columns ending with "Key" or "ID" across
* all tables in the model and disables summarization.
*
* Usage: Run this script on the entire model.
* CLI: te "workspace/model" script.csx --file
*
* Non-interactive: Yes (works on Model.Tables)
*/
var hiddenCount = 0;
foreach(var table in Model.Tables) {
foreach(var column in table.Columns) {
if(column.Name.EndsWith("Key") || column.Name.EndsWith("ID") || column.Name.EndsWith(" ID")) {
column.IsHidden = true;
column.SummarizeBy = AggregateFunction.None;
hiddenCount++;
}
}
}
Info("Hidden " + hiddenCount + " key/ID columns");
// Example: Hide Key Columns and Disable Summarization
// This script hides all ID/Key columns and disables summarization for all columns
Info("Starting model cleanup...");
var hiddenCount = 0;
var disabledCount = 0;
foreach(var table in Model.Tables) {
foreach(var column in table.Columns) {
// Disable summarization for all columns
column.SummarizeBy = AggregateFunction.None;
disabledCount++;
// Hide columns that end with "Key", "ID", or start with "_"
if(column.Name.EndsWith("Key") ||
column.Name.EndsWith("ID") ||
column.Name.EndsWith("Id") ||
column.Name.StartsWith("_")) {
column.IsHidden = true;
hiddenCount++;
}
}
}
Info("Hidden " + hiddenCount + " key columns");
Info("Disabled summarization for " + disabledCount + " columns");
Info("Model cleanup complete!");
/*
* Title: Modify Calculated Column Expression
*
* Description: Updates DAX expressions for existing calculated columns.
*
* WHEN TO USE:
* - Fix errors in calculated column logic
* - Update business rules embedded in columns
* - Optimize calculated column expressions
* - Refactor column calculations
* - Format DAX expressions for readability
*
* Usage: Configure column updates below.
* CLI: te "workspace/model" modify-calculated-column-expression.csx --file
*
* Non-interactive: Yes
*/
// ============================================================================
// CONFIGURATION
// ============================================================================
var tableName = "Sales";
// Map: Column Name → New Expression
var updatedExpressions = new Dictionary<string, string>
{
// Update to use DIVIDE instead of division operator
{ "Margin %", "DIVIDE([Revenue] - [Cost], [Revenue])" },
// Add error handling to date calculation
{ "Days to Ship", "IF(ISBLANK([ShipDate]), BLANK(), DATEDIFF([OrderDate], [ShipDate], DAY))" },
// Refine segmentation logic
{ "Revenue Segment", @"
SWITCH(
TRUE(),
[Revenue] >= 10000, ""Enterprise"",
[Revenue] >= 1000, ""Large"",
[Revenue] >= 100, ""Medium"",
""Small""
)" },
// Add null handling
{ "Profit", "IF(OR(ISBLANK([Revenue]), ISBLANK([Cost])), BLANK(), [Revenue] - [Cost])" }
};
// Format DAX after updating (recommended)
var formatDax = true;
// ============================================================================
// SCRIPT LOGIC
// ============================================================================
var table = Model.Tables[tableName];
var updatedCount = 0;
foreach(var entry in updatedExpressions)
{
var columnName = entry.Key;
var newExpression = entry.Value;
if(!table.Columns.Contains(columnName))
{
Info("⚠ Column not found: " + columnName);
continue;
}
var column = table.Columns[columnName];
// Verify it's a calculated column
if(column is CalculatedColumn)
{
var calcCol = column as CalculatedColumn;
// Store old expression for logging
var oldExpression = calcCol.Expression;
// Update expression
calcCol.Expression = newExpression;
// Format if requested
if(formatDax)
{
calcCol.Expression = FormatDax(calcCol.Expression);
}
updatedCount++;
Info("✓ Updated: " + columnName);
Info(" Old: " + oldExpression.Replace("\n", " ").Substring(0, Math.Min(50, oldExpression.Length)) + "...");
Info(" New: " + newExpression.Replace("\n", " ").Substring(0, Math.Min(50, newExpression.Length)) + "...");
}
else
{
Info("⚠ Not a calculated column: " + columnName + " (type: " + column.GetType().Name + ")");
}
}
Info("\nUpdated " + updatedCount + " calculated column expressions in " + tableName);
// ============================================================================
// BULK OPERATION: FORMAT ALL CALCULATED COLUMNS
// ============================================================================
Info("\nFormatting all calculated columns...");
var formattedCount = 0;
foreach(var column in table.Columns)
{
if(column is CalculatedColumn)
{
var calcCol = column as CalculatedColumn;
calcCol.Expression = FormatDax(calcCol.Expression);
formattedCount++;
}
}
Info("Formatted " + formattedCount + " calculated columns");
// ============================================================================
// NOTES
// ============================================================================
Info("\nBEST PRACTICES:");
Info(" - Use DIVIDE() instead of / to handle division by zero");
Info(" - Add ISBLANK() checks for nullable columns");
Info(" - Format DAX for readability");
Info(" - Test expressions after updating");
Info(" - Consider moving complex logic to measures if possible");
Columns Scripts
Scripts for managing columns in Tabular models.
Available Scripts
add_calculated_column.csx- Add calculated columns to tablesdisable_summarization.csx- Disable default summarization for columnshide_key_columns.csx- Hide key and ID columns across modelhide_keys.csx- Hide key columns in specific tablesset_data_category.csx- Set data categories for geographic columnsset-column-properties.csx- Set multiple column properties at onceset-column-data-type.csx- Change column data typesset-column-summarizeby.csx- Configure aggregation behaviorset-column-sortby.csx- Set sort-by columnsset-column-format-string.csx- Apply format strings to columnsset-source-column.csx- Map column to sourcedisable-available-in-mdx.csx- Disable MDX availabilityset-group-by-columns.csx- Configure group-by columnsset-alignment.csx- Set text alignmentset-is-key.csx- Mark columns as keysset-is-nullable.csx- Configure null handlingset-is-unique.csx- Mark unique columnsset-description.csx- Add column descriptionsset-is-default-image.csx- Set default image columns
Usage Examples
Execute Inline
te "model.bim" 'foreach(var col in Model.AllColumns.Where(c => c.Name.EndsWith("ID"))) { col.IsHidden = true; }'Execute Script File
te "model.bim" samples/columns/hide_key_columns.csx --file
te "Production/Sales" samples/columns/disable_summarization.csx --fileWith Fabric CLI Workflow
# Export model
fab export "Workspace/Model.SemanticModel" -o ./model -f
# Run column scripts
te "./model/Model.SemanticModel/model.bim" samples/columns/hide_key_columns.csx --file
te "./model/Model.SemanticModel/model.bim" samples/columns/set_data_category.csx --file
# Import back
fab import "Workspace/Model.SemanticModel" -i ./model/Model.SemanticModel -fCommon Patterns
Hide Key Columns
foreach(var table in Model.Tables) {
foreach(var column in table.Columns) {
if(column.Name.EndsWith("Key") || column.Name.EndsWith("ID")) {
column.IsHidden = true;
column.SummarizeBy = AggregateFunction.None;
}
}
}Disable Summarization
// Disable for all columns
foreach(var column in Model.AllColumns) {
column.SummarizeBy = AggregateFunction.None;
}
// Only for text columns
foreach(var column in Model.AllColumns.Where(c => c.DataType == DataType.String)) {
column.SummarizeBy = AggregateFunction.None;
}Set Data Categories
// Mark geographic columns
Model.Tables["Geography"].Columns["Country"].DataCategory = "Country";
Model.Tables["Geography"].Columns["State"].DataCategory = "StateOrProvince";
Model.Tables["Geography"].Columns["City"].DataCategory = "City";
Model.Tables["Geography"].Columns["Postal Code"].DataCategory = "PostalCode";Add Calculated Column
var table = Model.Tables["Sales"];
var col = table.AddCalculatedColumn("Full Name");
col.Expression = "[First Name] & \" \" & [Last Name]";
col.DataType = DataType.String;
col.IsHidden = false;Set Sort-By Column
// Sort month names by month number
var monthName = Model.Tables["Date"].Columns["Month Name"];
var monthNumber = Model.Tables["Date"].Columns["Month Number"];
monthName.SortByColumn = monthNumber;Apply Format Strings
// Format date columns
foreach(var col in Model.AllColumns.Where(c => c.DataType == DataType.DateTime)) {
col.FormatString = "mm/dd/yyyy";
}
// Format currency columns
foreach(var col in Model.AllColumns.Where(c => c.Name.Contains("Amount"))) {
col.FormatString = "$#,0.00";
}Property Reference
Column Properties
Name- Column nameDescription- Column descriptionDataType- Data type (String, Int64, Double, DateTime, Boolean, etc.)IsHidden- Visibility flagDisplayFolder- Display folder pathFormatString- Number/date formatDataCategory- Data category (Country, City, WebUrl, ImageUrl, etc.)SummarizeBy- Default aggregation (None, Sum, Min, Max, Count, etc.)SortByColumn- Column used for sortingIsKey- Mark as key columnIsNullable- Allow null valuesIsUnique- Mark as uniqueTable- Parent table reference
Data Types
DataType.String- TextDataType.Int64- IntegerDataType.Double- DecimalDataType.DateTime- Date/timeDataType.Boolean- True/falseDataType.Decimal- Precise decimal
Data Categories
"Country"- Country names"StateOrProvince"- State/province"City"- City names"PostalCode"- Postal codes"Continent"- Continents"WebUrl"- Web URLs"ImageUrl"- Image URLs"Latitude"- Geographic latitude"Longitude"- Geographic longitude
Aggregate Functions
AggregateFunction.None- No aggregationAggregateFunction.Sum- Sum valuesAggregateFunction.Count- Count rowsAggregateFunction.Min- Minimum valueAggregateFunction.Max- Maximum valueAggregateFunction.Average- Average value
Best Practices
1. Hide Technical Columns
- Hide all key/ID columns
- Hide intermediate calculated columns
- Use IsHidden to reduce clutter
2. Disable Summarization
- Disable for text columns
- Disable for key columns
- Only enable for numeric measures
3. Data Categories
- Use for geographic columns
- Enables map visualizations
- Use ImageUrl for image columns
4. Sort-By Columns
- Sort month names by month number
- Sort custom orderings
- Improves user experience
See Also
- Tables
- Measures
- Format Strings
- Display Folders
// Example: Set Column Data Category
// This script marks geographic columns with appropriate data categories
// Mark geographic columns
Model.Tables["Locations"].Columns["Country"].DataCategory = "Country";
Model.Tables["Locations"].Columns["State"].DataCategory = "StateOrProvince";
Model.Tables["Locations"].Columns["City"].DataCategory = "City";
Model.Tables["Locations"].Columns["Postal Code"].DataCategory = "PostalCode";
Info("Set data categories for location columns");
/*
* Title: Set Column Alignment
*
* Description: Sets the text alignment for columns in report visualizations.
* Controls how text appears in table/matrix visuals.
*
* WHEN TO USE:
* - Align currency values to the right for better readability
* - Center-align headers or categorical text
* - Left-align descriptive text fields
* - Ensure consistent visual formatting across reports
*
* Usage: Configure alignments below.
* CLI: te "workspace/model" set-alignment.csx --file
*
* Non-interactive: Yes
*/
// ============================================================================
// CONFIGURATION
// ============================================================================
var tableName = "Sales";
// Map: Column Name → Alignment
// Valid values: Default, Left, Right, Center
var columnAlignments = new Dictionary<string, Alignment>
{
// Right-align numeric/currency columns
{ "Revenue", Alignment.Right },
{ "Quantity", Alignment.Right },
{ "UnitPrice", Alignment.Right },
{ "DiscountPercent", Alignment.Right },
// Left-align text columns
{ "ProductName", Alignment.Left },
{ "CustomerName", Alignment.Left },
{ "Description", Alignment.Left },
// Center-align dates or categorical values
{ "OrderDate", Alignment.Center },
{ "Status", Alignment.Center },
{ "Category", Alignment.Center }
};
// ============================================================================
// SCRIPT LOGIC
// ============================================================================
var table = Model.Tables[tableName];
var updatedCount = 0;
foreach(var entry in columnAlignments)
{
var columnName = entry.Key;
var alignment = entry.Value;
if(table.Columns.Contains(columnName))
{
table.Columns[columnName].Alignment = alignment;
updatedCount++;
Info("✓ " + columnName + " → " + alignment);
}
else
{
Info("⚠ Column not found: " + columnName);
}
}
Info("\nSet alignment for " + updatedCount + " columns in " + tableName);
// ============================================================================
// REFERENCE
// ============================================================================
Info("\nALIGNMENT VALUES:");
Info(" Alignment.Default - Use client default (usually left)");
Info(" Alignment.Left - Left-align (text, descriptions)");
Info(" Alignment.Right - Right-align (numbers, currency)");
Info(" Alignment.Center - Center-align (dates, categories)");
/*
* Title: Set Column Data Type
*
* Description: Sets the DataType property for columns. Use when importing
* tables or correcting data type inference issues.
*
* Usage: Configure target columns and desired data types below.
* CLI: te "workspace/model" set-column-data-type.csx --file
*
* Non-interactive: Yes
*/
// ============================================================================
// CONFIGURATION
// ============================================================================
var tableName = "Sales";
// Define columns and their target data types
var columnTypes = new Dictionary<string, DataType>
{
{ "CustomerKey", DataType.Int64 },
{ "OrderDate", DataType.DateTime },
{ "ShipDate", DataType.DateTime },
{ "Revenue", DataType.Decimal },
{ "Quantity", DataType.Int64 },
{ "UnitPrice", DataType.Decimal },
{ "DiscountPercent", DataType.Double },
{ "ProductName", DataType.String },
{ "IsActive", DataType.Boolean }
};
// ============================================================================
// SCRIPT LOGIC
// ============================================================================
var table = Model.Tables[tableName];
var updatedCount = 0;
foreach(var entry in columnTypes)
{
var columnName = entry.Key;
var dataType = entry.Value;
if(table.Columns.Contains(columnName))
{
var column = table.Columns[columnName];
column.DataType = dataType;
updatedCount++;
Info("Set " + columnName + " → " + dataType);
}
else
{
Info("⚠ Column not found: " + columnName);
}
}
Info("\nUpdated data types for " + updatedCount + " columns in " + tableName);
/*
* Title: Set Column Format Strings
*
* Description: Applies format strings to columns based on data type and
* naming patterns. Common formats: currency, percentage, dates, numbers.
*
* Usage: Configure format patterns below.
* CLI: te "workspace/model" set-column-format-string.csx --file
*
* Non-interactive: Yes
*/
// ============================================================================
// CONFIGURATION
// ============================================================================
var tableName = "Sales";
// Option 1: Explicit column formats
var columnFormats = new Dictionary<string, string>
{
{ "OrderDate", "mm/dd/yyyy" },
{ "ShipDate", "mm/dd/yyyy" },
{ "Revenue", "$#,##0.00" },
{ "Cost", "$#,##0.00" },
{ "UnitPrice", "$#,##0.00" },
{ "Quantity", "#,##0" },
{ "DiscountPercent", "0.00%" },
{ "MarginPercent", "0.0%" }
};
// Option 2: Pattern-based formatting (uncomment to use)
var usePatternBased = false;
// ============================================================================
// SCRIPT LOGIC
// ============================================================================
var table = Model.Tables[tableName];
var updatedCount = 0;
if(!usePatternBased)
{
// Option 1: Explicit configuration
foreach(var entry in columnFormats)
{
var columnName = entry.Key;
var formatString = entry.Value;
if(table.Columns.Contains(columnName))
{
table.Columns[columnName].FormatString = formatString;
updatedCount++;
Info("✓ " + columnName + " → " + formatString);
}
else
{
Info("⚠ Column not found: " + columnName);
}
}
}
else
{
// Option 2: Pattern-based formatting
foreach(var column in table.Columns)
{
// Skip if already has format string
if(!string.IsNullOrEmpty(column.FormatString)) continue;
// Date columns
if(column.DataType == DataType.DateTime)
{
column.FormatString = "mm/dd/yyyy";
updatedCount++;
}
// Currency columns (by name pattern)
else if(column.Name.Contains("Revenue") ||
column.Name.Contains("Cost") ||
column.Name.Contains("Price") ||
column.Name.Contains("Amount") ||
column.Name.StartsWith("$"))
{
column.FormatString = "$#,##0.00";
updatedCount++;
}
// Percentage columns (by name pattern)
else if(column.Name.Contains("Percent") ||
column.Name.Contains("Rate") ||
column.Name.Contains("%") ||
column.Name.EndsWith("Pct"))
{
column.FormatString = "0.00%";
updatedCount++;
}
// Whole number columns
else if(column.DataType == DataType.Int64)
{
column.FormatString = "#,##0";
updatedCount++;
}
// Decimal columns (generic)
else if(column.DataType == DataType.Decimal ||
column.DataType == DataType.Double)
{
column.FormatString = "#,##0.00";
updatedCount++;
}
}
}
Info("\nApplied format strings to " + updatedCount + " columns in " + tableName);
// ============================================================================
// COMMON FORMAT STRING REFERENCE
// ============================================================================
Info("\nCommon Format Strings:" +
"\n Currency (2 decimal): $#,##0.00" +
"\n Currency (no decimal): $#,0" +
"\n Percentage (2 decimal): 0.00%" +
"\n Percentage (1 decimal): 0.0%" +
"\n Percentage (no decimal): 0%" +
"\n Whole number: #,##0" +
"\n Decimal (2 places): #,##0.00" +
"\n Date (US): mm/dd/yyyy" +
"\n Date (ISO): yyyy-MM-dd" +
"\n Date (readable): MMM dd, yyyy" +
"\n Millions/Thousands: [>=1000000]$0.0,,\"M\";[>=1000]$0.0,\"K\";$#,0");
/*
* Title: Set Column Properties (Comprehensive Example)
*
* Description: Demonstrates how to set all common column properties including
* DataType, SummarizeBy, FormatString, SortByColumn, IsHidden, and more.
*
* Usage: Modify configuration section, then run.
* CLI: te "workspace/model" set-column-properties.csx --file
*
* Non-interactive: Yes
*/
// ============================================================================
// CONFIGURATION
// ============================================================================
var tableName = "Sales";
// ============================================================================
// EXAMPLE 1: Configure Key/ID Column
// ============================================================================
var customerKeyCol = Model.Tables[tableName].Columns["CustomerKey"];
customerKeyCol.DataType = DataType.Int64;
customerKeyCol.IsKey = false; // Usually false unless it's THE primary key
customerKeyCol.IsHidden = true;
customerKeyCol.SummarizeBy = AggregateFunction.None;
customerKeyCol.DisplayFolder = "Columns/Keys";
customerKeyCol.Description = "Customer identifier (foreign key)";
customerKeyCol.IsAvailableInMDX = false;
Info("Configured CustomerKey column");
// ============================================================================
// EXAMPLE 2: Configure Date Column
// ============================================================================
var orderDateCol = Model.Tables[tableName].Columns["OrderDate"];
orderDateCol.DataType = DataType.DateTime;
orderDateCol.FormatString = "mm/dd/yyyy";
orderDateCol.SummarizeBy = AggregateFunction.None;
orderDateCol.DisplayFolder = "Columns/Dates";
orderDateCol.Description = "Date when order was placed";
Info("Configured OrderDate column");
// ============================================================================
// EXAMPLE 3: Configure Currency Column
// ============================================================================
var revenueCol = Model.Tables[tableName].Columns["Revenue"];
revenueCol.DataType = DataType.Decimal;
revenueCol.FormatString = "$#,##0.00";
revenueCol.SummarizeBy = AggregateFunction.Sum;
revenueCol.DisplayFolder = "Columns/Metrics";
revenueCol.Description = "Total revenue amount in USD";
Info("Configured Revenue column");
// ============================================================================
// EXAMPLE 4: Configure Percentage Column
// ============================================================================
var discountPctCol = Model.Tables[tableName].Columns["DiscountPercent"];
discountPctCol.DataType = DataType.Double;
discountPctCol.FormatString = "0.00%";
discountPctCol.SummarizeBy = AggregateFunction.Average;
discountPctCol.DisplayFolder = "Columns/Metrics";
discountPctCol.Description = "Discount percentage applied to order";
Info("Configured DiscountPercent column");
// ============================================================================
// EXAMPLE 5: Configure Text/Name Column
// ============================================================================
var productNameCol = Model.Tables[tableName].Columns["ProductName"];
productNameCol.DataType = DataType.String;
productNameCol.SummarizeBy = AggregateFunction.None;
productNameCol.DisplayFolder = "Columns/Names";
productNameCol.Description = "Product name from catalog";
Info("Configured ProductName column");
// ============================================================================
// EXAMPLE 6: Configure Sort By Column
// ============================================================================
// Month name sorted by month number
var monthNameCol = Model.Tables[tableName].Columns["MonthName"];
var monthNumCol = Model.Tables[tableName].Columns["MonthNumber"];
monthNameCol.DataType = DataType.String;
monthNameCol.SummarizeBy = AggregateFunction.None;
monthNameCol.SortByColumn = monthNumCol; // Sort "Jan", "Feb" by 1, 2
monthNumCol.DataType = DataType.Int64;
monthNumCol.IsHidden = true; // Hide the sort column
monthNumCol.SummarizeBy = AggregateFunction.None;
Info("Configured MonthName with SortByColumn");
// ============================================================================
// EXAMPLE 7: Configure Boolean/Flag Column
// ============================================================================
var isActiveCol = Model.Tables[tableName].Columns["IsActive"];
isActiveCol.DataType = DataType.Boolean;
isActiveCol.SummarizeBy = AggregateFunction.None;
isActiveCol.DisplayFolder = "Columns/Attributes";
isActiveCol.Description = "Indicates if record is currently active";
Info("Configured IsActive column");
// ============================================================================
// EXAMPLE 8: Configure Whole Number Column
// ============================================================================
var quantityCol = Model.Tables[tableName].Columns["Quantity"];
quantityCol.DataType = DataType.Int64;
quantityCol.FormatString = "#,##0";
quantityCol.SummarizeBy = AggregateFunction.Sum;
quantityCol.DisplayFolder = "Columns/Metrics";
quantityCol.Description = "Quantity of items ordered";
Info("Configured Quantity column");
// ============================================================================
// SUMMARY
// ============================================================================
Info("Column configuration complete!\n\n" +
"Configured:\n" +
"- Key columns (CustomerKey)\n" +
"- Date columns (OrderDate)\n" +
"- Currency columns (Revenue)\n" +
"- Percentage columns (DiscountPercent)\n" +
"- Text columns (ProductName)\n" +
"- Sort relationships (MonthName sorted by MonthNumber)\n" +
"- Boolean columns (IsActive)\n" +
"- Integer columns (Quantity)");
/*
* Title: Set Column SortByColumn Property
*
* Description: Configures columns to sort by other columns. Common patterns:
* - Month names sorted by month numbers
* - Weekday names sorted by weekday numbers
* - Custom categories sorted by order column
*
* Usage: Define sort relationships below.
* CLI: te "workspace/model" set-column-sortby.csx --file
*
* Non-interactive: Yes
*/
// ============================================================================
// CONFIGURATION
// ============================================================================
var tableName = "Date";
// Define sort relationships: DisplayColumn → SortByColumn
var sortRelationships = new Dictionary<string, string>
{
// Month columns
{ "MonthName", "MonthNumber" }, // "January" sorted by 1
{ "MonthShort", "MonthNumber" }, // "Jan" sorted by 1
{ "MonthYear", "YearMonth" }, // "Jan 2024" sorted by 202401
// Weekday columns
{ "WeekdayName", "WeekdayNumber" }, // "Monday" sorted by 1
{ "WeekdayShort", "WeekdayNumber" }, // "Mon" sorted by 1
// Quarter columns
{ "QuarterYear", "YearQuarter" }, // "Q1 2024" sorted by 202401
// Year columns
{ "YearName", "YearNumber" }, // "2024" sorted by 2024
// Week columns
{ "WeekLabel", "WeekNumber" } // "Week 25" sorted by 25
};
// Auto-hide sort columns (recommended)
var hideSortColumns = true;
// ============================================================================
// SCRIPT LOGIC
// ============================================================================
var table = Model.Tables[tableName];
var updatedCount = 0;
var hiddenCount = 0;
foreach(var entry in sortRelationships)
{
var displayColumnName = entry.Key;
var sortColumnName = entry.Value;
// Check both columns exist
if(!table.Columns.Contains(displayColumnName))
{
Info("⚠ Display column not found: " + displayColumnName);
continue;
}
if(!table.Columns.Contains(sortColumnName))
{
Info("⚠ Sort column not found: " + sortColumnName);
continue;
}
var displayColumn = table.Columns[displayColumnName];
var sortColumn = table.Columns[sortColumnName];
// Set the sort relationship
displayColumn.SortByColumn = sortColumn;
updatedCount++;
// Optionally hide the sort column
if(hideSortColumns && !sortColumn.IsHidden)
{
sortColumn.IsHidden = true;
sortColumn.IsAvailableInMDX = false;
hiddenCount++;
}
Info("✓ " + displayColumnName + " sorted by " + sortColumnName);
}
Info("\nConfigured " + updatedCount + " sort relationships in " + tableName);
if(hideSortColumns)
{
Info("Hidden " + hiddenCount + " sort columns");
}
// ============================================================================
// VALIDATION
// ============================================================================
Info("\nIMPORTANT: Sort column must have unique values for each display value.");
Info("Example: Each 'January' must have exactly one corresponding '1'");
/*
* Title: Set Column SummarizeBy Property
*
* Description: Controls automatic aggregation behavior for columns in visuals.
* Best practice: Set to None for keys, IDs, dates, and text. Use Sum only
* for actual numeric metrics.
*
* Usage: Configure aggregation rules below.
* CLI: te "workspace/model" set-column-summarizeby.csx --file
*
* Non-interactive: Yes
*/
// ============================================================================
// CONFIGURATION
// ============================================================================
var tableName = "Sales";
// Option 1: Set specific columns
var columnAggregations = new Dictionary<string, AggregateFunction>
{
{ "CustomerKey", AggregateFunction.None },
{ "ProductKey", AggregateFunction.None },
{ "OrderDate", AggregateFunction.None },
{ "Revenue", AggregateFunction.Sum },
{ "Quantity", AggregateFunction.Sum },
{ "DiscountPercent", AggregateFunction.Average },
{ "ProductName", AggregateFunction.None }
};
// Option 2: Pattern-based rules (uncomment to use)
var usePatternBased = false;
// ============================================================================
// SCRIPT LOGIC
// ============================================================================
var table = Model.Tables[tableName];
var updatedCount = 0;
if(!usePatternBased)
{
// Option 1: Explicit column configuration
foreach(var entry in columnAggregations)
{
var columnName = entry.Key;
var aggregation = entry.Value;
if(table.Columns.Contains(columnName))
{
table.Columns[columnName].SummarizeBy = aggregation;
updatedCount++;
}
}
}
else
{
// Option 2: Pattern-based configuration
foreach(var column in table.Columns)
{
// Keys and IDs - no aggregation
if(column.Name.EndsWith("Key") || column.Name.EndsWith("ID"))
{
column.SummarizeBy = AggregateFunction.None;
updatedCount++;
}
// Dates - no aggregation
else if(column.DataType == DataType.DateTime)
{
column.SummarizeBy = AggregateFunction.None;
updatedCount++;
}
// Text - no aggregation
else if(column.DataType == DataType.String)
{
column.SummarizeBy = AggregateFunction.None;
updatedCount++;
}
// Boolean - no aggregation
else if(column.DataType == DataType.Boolean)
{
column.SummarizeBy = AggregateFunction.None;
updatedCount++;
}
// Numeric columns - check naming patterns
else if(column.DataType == DataType.Decimal ||
column.DataType == DataType.Double ||
column.DataType == DataType.Int64)
{
// Percentages - average
if(column.Name.Contains("Percent") || column.Name.Contains("Rate"))
{
column.SummarizeBy = AggregateFunction.Average;
updatedCount++;
}
// Other numeric - sum
else
{
column.SummarizeBy = AggregateFunction.Sum;
updatedCount++;
}
}
}
}
Info("Updated SummarizeBy for " + updatedCount + " columns in " + tableName);
/*
* Title: Set Data Category
*
* Description: Sets the DataCategory property for columns to enable custom
* behaviors in Power BI. Common use cases: geographic data, images, URLs.
*
* Usage: Configure data categories below.
* CLI: te "workspace/model" set-data-category.csx --file
*
* Non-interactive: Yes
*/
// ============================================================================
// CONFIGURATION
// ============================================================================
var tableName = "Locations";
// Map: Column Name → Data Category
// See full list: https://learn.microsoft.com/en-us/dotnet/api/microsoft.analysisservices.datacategory
var dataCategoryMappings = new Dictionary<string, string>
{
// Geographic categories
{ "Continent", "Continent" },
{ "Country", "Country" },
{ "StateProvince", "StateOrProvince" },
{ "City", "City" },
{ "PostalCode", "PostalCode" },
{ "County", "County" },
{ "Latitude", "Latitude" },
{ "Longitude", "Longitude" },
{ "Address", "Address" },
// Image categories
{ "ProductImageURL", "ImageURL" },
{ "ProductImage", "Image" },
// Web categories
{ "WebsiteURL", "WebURL" },
// Other categories
{ "BarcodeImage", "Barcode" }
};
// ============================================================================
// SCRIPT LOGIC
// ============================================================================
var table = Model.Tables[tableName];
var updatedCount = 0;
foreach(var entry in dataCategoryMappings)
{
var columnName = entry.Key;
var dataCategory = entry.Value;
if(table.Columns.Contains(columnName))
{
table.Columns[columnName].DataCategory = dataCategory;
updatedCount++;
Info("✓ " + columnName + " → " + dataCategory);
}
else
{
Info("⚠ Column not found: " + columnName);
}
}
Info("\nSet DataCategory for " + updatedCount + " columns in " + tableName);
// ============================================================================
// COMMON DATA CATEGORY REFERENCE
// ============================================================================
Info("\nCOMMON DATA CATEGORIES:");
Info("\nGeographic:");
Info(" - Continent, Country, StateOrProvince, County");
Info(" - City, PostalCode, Address");
Info(" - Latitude, Longitude");
Info("\nImages:");
Info(" - Image, ImageURL");
Info(" - ImageBMP, ImageGIF, ImageJPG, ImagePNG, ImageTIFF");
Info("\nWeb:");
Info(" - WebURL");
Info("\nOther:");
Info(" - Barcode");
Info(" - Person, Place, Product, Organization");
Info("\nNOTE: There are 248 total data categories.");
Info("See MS-SSAS-T documentation for complete list.");
// ============================================================================
// NOTES
// ============================================================================
Info("\nUSE CASES:");
Info("- Geographic: Enables map visualizations in Power BI");
Info("- Image/ImageURL: Display images in table visuals");
Info("- WebURL: Create clickable links in reports");
Info("- Barcode: Special formatting for barcode display");
/*
* Title: Set Column Descriptions
*
* Description: Adds documentation to columns for maintainability and governance.
*
* WHEN TO USE:
* - Document column purpose and business meaning
* - Provide context for report authors and analysts
* - Support data governance and compliance
* - Improve model maintainability
* - Explain business rules, calculations, or transformations
* - Document data lineage and source information
*
* Usage: Configure column descriptions below.
* CLI: te "workspace/model" set-description.csx --file
*
* Non-interactive: Yes
*/
// ============================================================================
// CONFIGURATION
// ============================================================================
var tableName = "Sales";
// Map: Column Name → Description
var columnDescriptions = new Dictionary<string, string>
{
{ "OrderID", "Unique order identifier from source ERP system" },
{ "CustomerKey", "Foreign key to DimCustomer dimension table" },
{ "ProductKey", "Foreign key to DimProduct dimension table" },
{ "OrderDate", "Date when customer placed the order" },
{ "ShipDate", "Date when order was shipped (may be null for pending orders)" },
{ "Revenue", "Total revenue amount in USD, calculated as Quantity × UnitPrice × (1 - DiscountPercent)" },
{ "Quantity", "Number of units ordered" },
{ "UnitPrice", "Price per unit at time of order" },
{ "DiscountPercent", "Discount percentage applied (0.00 to 1.00), may be null if no discount" },
{ "Cost", "Total cost of goods sold for this order" },
{ "Profit", "Calculated as Revenue - Cost" }
};
// ============================================================================
// SCRIPT LOGIC
// ============================================================================
var table = Model.Tables[tableName];
var updatedCount = 0;
foreach(var entry in columnDescriptions)
{
var columnName = entry.Key;
var description = entry.Value;
if(table.Columns.Contains(columnName))
{
table.Columns[columnName].Description = description;
updatedCount++;
Info("✓ " + columnName);
}
else
{
Info("⚠ Column not found: " + columnName);
}
}
Info("\nAdded descriptions to " + updatedCount + " columns in " + tableName);
// ============================================================================
// BEST PRACTICES
// ============================================================================
Info("\nDESCRIPTION BEST PRACTICES:");
Info(" - Include business meaning, not just technical details");
Info(" - Document calculation logic for derived columns");
Info(" - Specify units (USD, units, percentages, etc.)");
Info(" - Note nullable fields and their business meaning");
Info(" - Reference related tables for foreign keys");
Info(" - Document data lineage (source system, transformations)");
Info(" - Keep descriptions concise but informative");
/*
* Title: Set EncodingHint Property
*
* Description: Hints the server on numeric encoding strategy for optimization.
*
* WHEN TO USE:
* - Optimize memory and performance for numeric columns on Azure Analysis Services or SQL Server Analysis Services
* - Override automatic encoding when you know the data pattern better
* - Value encoding: For columns with few distinct values (e.g., status codes, flags)
* - Hash encoding: For high-cardinality numeric columns (e.g., IDs, keys)
* - Improve compression ratios for large datasets
* - Fine-tune query performance based on usage patterns
*
* IMPORTANT: Power BI IGNORES EncodingHint - it uses its own automatic encoding.
* This property only affects Azure Analysis Services and SQL Server Analysis Services.
* Setting this in Power BI models will have NO EFFECT.
*
* REQUIRES: Compatibility level 1400+
*
* Usage: Configure encoding hints below (only for AAS/SSAS models).
* CLI: te "workspace/model" set-encoding-hint.csx --file
*
* Non-interactive: Yes
*/
// ============================================================================
// CONFIGURATION
// ============================================================================
var tableName = "FactSales";
// Map: Column Name → EncodingHint
// Valid values: Default, Value, Hash
var encodingHints = new Dictionary<string, EncodingHintType>
{
// Value encoding: Few distinct values (better compression, faster aggregation)
{ "StatusCode", EncodingHintType.Value }, // e.g., 1, 2, 3 for Pending/Shipped/Completed
{ "PriorityLevel", EncodingHintType.Value }, // e.g., 1-5 priority levels
{ "IsPromotional", EncodingHintType.Value }, // Boolean stored as 0/1
{ "RegionID", EncodingHintType.Value }, // Limited number of regions
// Hash encoding: High cardinality (better for unique values)
{ "OrderID", EncodingHintType.Hash }, // Unique order identifiers
{ "TransactionID", EncodingHintType.Hash }, // Unique transaction IDs
{ "CustomerKey", EncodingHintType.Hash }, // Many unique customers
{ "ProductKey", EncodingHintType.Hash } // Many unique products
};
// ============================================================================
// SCRIPT LOGIC
// ============================================================================
var table = Model.Tables[tableName];
var updatedCount = 0;
foreach(var entry in encodingHints)
{
var columnName = entry.Key;
var encodingHint = entry.Value;
if(table.Columns.Contains(columnName))
{
var column = table.Columns[columnName];
// Verify this is a numeric column
if(column.DataType == DataType.Int64 ||
column.DataType == DataType.Decimal ||
column.DataType == DataType.Double)
{
column.EncodingHint = encodingHint;
updatedCount++;
Info("✓ " + columnName + " → " + encodingHint);
}
else
{
Info("⚠ Skipped (not numeric): " + columnName + " (" + column.DataType + ")");
}
}
else
{
Info("⚠ Column not found: " + columnName);
}
}
Info("\nSet EncodingHint for " + updatedCount + " columns in " + tableName);
// ============================================================================
// REFERENCE
// ============================================================================
Info("\nENCODING HINT TYPES:");
Info(" Default - Let server auto-detect (recommended for most cases)");
Info(" Value - For low-cardinality columns (few distinct values)");
Info(" Examples: Status codes, flags, categories (< 1000 values)");
Info(" Hash - For high-cardinality columns (many unique values)");
Info(" Examples: IDs, keys, unique identifiers");
Info("");
Info("PERFORMANCE IMPACT:");
Info(" - Value encoding: Better compression, faster aggregations");
Info(" - Hash encoding: Better for point lookups and joins");
Info(" - Wrong encoding can hurt performance - test and measure!");
Info("");
Info("NOTE: Requires compatibility level 1400 or higher");
/*
* Title: Set Group By Columns
*
* Description: Configures the GroupByColumns property for field parameters.
* When a column is used in visuals, Power BI will automatically group by
* the columns in this collection as well.
*
* Primary use case: Field parameters (dynamic column selection)
*
* Usage: Configure group-by relationships below.
* CLI: te "workspace/model" set-group-by-columns.csx --file
*
* Non-interactive: Yes
*/
// ============================================================================
// CONFIGURATION
// ============================================================================
var tableName = "Field Parameter";
// Map: Display Column → GroupBy Column
// When display column is used, also group by the specified column
var groupByMappings = new Dictionary<string, string>
{
// Field parameter pattern: Name column groups by Fields column
{ "Field Parameter", "Field Parameter Fields" }
};
// ============================================================================
// SCRIPT LOGIC
// ============================================================================
var table = Model.Tables[tableName];
var updatedCount = 0;
foreach(var entry in groupByMappings)
{
var displayColumnName = entry.Key;
var groupByColumnName = entry.Value;
// Validate both columns exist
if(!table.Columns.Contains(displayColumnName))
{
Info("⚠ Display column not found: " + displayColumnName);
continue;
}
if(!table.Columns.Contains(groupByColumnName))
{
Info("⚠ GroupBy column not found: " + groupByColumnName);
continue;
}
var displayColumn = table.Columns[displayColumnName];
var groupByColumn = table.Columns[groupByColumnName];
// Add to GroupByColumns collection (if not already present)
if(!displayColumn.GroupByColumns.Contains(groupByColumn))
{
displayColumn.GroupByColumns.Add(groupByColumn);
updatedCount++;
Info("✓ " + displayColumnName + " will group by " + groupByColumnName);
}
else
{
Info("⚠ Already configured: " + displayColumnName + " → " + groupByColumnName);
}
}
Info("\nConfigured GroupByColumns for " + updatedCount + " columns in " + tableName);
// ============================================================================
// FIELD PARAMETER COMPLETE CONFIGURATION EXAMPLE
// ============================================================================
Info("\nFIELD PARAMETER PATTERN:");
Info("For proper field parameters, you need:");
Info(" 1. Name column (display)");
Info(" 2. Fields column (DAX expression, hidden)");
Info(" 3. Order column (sort order, hidden)");
Info("");
Info("Configuration:");
Info(" nameColumn.SortByColumn = orderColumn;");
Info(" nameColumn.GroupByColumns.Add(fieldColumn);");
Info(" fieldColumn.SortByColumn = orderColumn;");
Info(" fieldColumn.IsHidden = true;");
Info(" fieldColumn.SetExtendedProperty(\"ParameterMetadata\", \"{...}\", ExtendedPropertyType.Json);");
Info(" orderColumn.IsHidden = true;");
// ============================================================================
// NOTES
// ============================================================================
Info("\nNOTE:");
Info("- GroupByColumns is primarily used for field parameters");
Info("- When the display column is used in a visual, Power BI automatically");
Info(" groups by columns in the GroupByColumns collection");
Info("- See add-field-parameter.csx for complete field parameter setup");
/*
* Title: Set IsDefaultImage Property
*
* Description: Marks a column as the default image for the table in CSDL.
*
* WHEN TO USE:
* - Specify which image column represents the entity (product, person, etc.)
* - Enable Power BI to automatically use the image in card visuals
* - Improve user experience by setting representative images
* - Support entity visualization in composite models
*
* PREREQUISITE: Column must have DataCategory set to an image type
* (Image, ImageURL, ImageBMP, ImageGIF, ImageJPG, ImagePNG, ImageTIFF)
*
* Usage: Configure default image columns below.
* CLI: te "workspace/model" set-is-default-image.csx --file
*
* Non-interactive: Yes
*/
// ============================================================================
// CONFIGURATION
// ============================================================================
// Map: Table Name → Image Column Name
var defaultImageColumns = new Dictionary<string, string>
{
{ "DimProduct", "ProductImageURL" },
{ "DimEmployee", "EmployeePhoto" },
{ "DimCustomer", "CustomerAvatar" },
{ "DimStore", "StorePhoto" }
};
// ============================================================================
// SCRIPT LOGIC
// ============================================================================
var updatedCount = 0;
foreach(var entry in defaultImageColumns)
{
var tableName = entry.Key;
var imageColumnName = entry.Value;
if(!Model.Tables.Contains(tableName))
{
Info("⚠ Table not found: " + tableName);
continue;
}
var table = Model.Tables[tableName];
if(!table.Columns.Contains(imageColumnName))
{
Info("⚠ Column not found: " + tableName + "[" + imageColumnName + "]");
continue;
}
var column = table.Columns[imageColumnName];
// Set DataCategory if not already an image type
if(string.IsNullOrEmpty(column.DataCategory) ||
!column.DataCategory.StartsWith("Image"))
{
column.DataCategory = "ImageURL";
Info(" Set DataCategory = ImageURL for " + imageColumnName);
}
// Mark as default image
column.IsDefaultImage = true;
updatedCount++;
Info("✓ Set default image: " + tableName + "[" + imageColumnName + "]");
}
Info("\nConfigured " + updatedCount + " default image columns");
// ============================================================================
// NOTES
// ============================================================================
Info("\nREQUIREMENTS:");
Info(" - Column must have image DataCategory:");
Info(" Image, ImageURL, ImageBMP, ImageGIF, ImageJPG, ImagePNG, ImageTIFF");
Info(" - Only one column per table should be IsDefaultImage = true");
Info("");
Info("USE CASES:");
Info(" - Product images in catalog");
Info(" - Employee photos in org charts");
Info(" - Customer avatars");
Info(" - Store/location photos");
/*
* Title: Set IsDefaultLabel Property
*
* Description: Marks a column to be included in the DisplayKey element in CSDL.
* This designates the primary descriptive field for an entity.
*
* WHEN TO USE:
* - Specify the main display name for an entity (product name, customer name)
* - Improve entity representation in composite models
* - Enable better default labeling in Power BI visualizations
* - Support cross-report and cross-model entity references
*
* Usage: Configure default label columns below.
* CLI: te "workspace/model" set-is-default-label.csx --file
*
* Non-interactive: Yes
*/
// ============================================================================
// CONFIGURATION
// ============================================================================
// Map: Table Name → Label Column Name
var defaultLabelColumns = new Dictionary<string, string>
{
{ "DimProduct", "ProductName" },
{ "DimCustomer", "CustomerName" },
{ "DimEmployee", "EmployeeName" },
{ "DimStore", "StoreName" },
{ "DimCategory", "CategoryName" },
{ "DimDate", "Date" }
};
// ============================================================================
// SCRIPT LOGIC
// ============================================================================
var updatedCount = 0;
foreach(var entry in defaultLabelColumns)
{
var tableName = entry.Key;
var labelColumnName = entry.Value;
if(!Model.Tables.Contains(tableName))
{
Info("⚠ Table not found: " + tableName);
continue;
}
var table = Model.Tables[tableName];
if(!table.Columns.Contains(labelColumnName))
{
Info("⚠ Column not found: " + tableName + "[" + labelColumnName + "]");
continue;
}
var column = table.Columns[labelColumnName];
column.IsDefaultLabel = true;
updatedCount++;
Info("✓ Set default label: " + tableName + "[" + labelColumnName + "]");
}
Info("\nConfigured " + updatedCount + " default label columns");
// ============================================================================
// NOTES
// ============================================================================
Info("\nUSE CASES:");
Info(" - Primary display names for entities");
Info(" - Product names, customer names, employee names");
Info(" - Category descriptions");
Info(" - Date representations");
Info("");
Info("BEST PRACTICES:");
Info(" - Choose the most descriptive, user-friendly column");
Info(" - Only one column per table should be IsDefaultLabel = true");
Info(" - Use columns users would naturally identify the entity by");
Info(" - Improves cross-model entity representation");
/*
* Title: Set Column IsKey Property
*
* Description: Marks columns as table keys for referential integrity.
*
* WHEN TO USE:
* - Define primary key columns for dimension tables
* - Improve query performance by marking unique identifiers
* - Maintain referential integrity in relationships
* - Enable Power BI to optimize join operations
* - Document table structure for governance
*
* NOTE: Marking a column as IsKey automatically sets IsNullable = false
*
* Usage: Configure key columns below.
* CLI: te "workspace/model" set-is-key.csx --file
*
* Non-interactive: Yes
*/
// ============================================================================
// CONFIGURATION
// ============================================================================
// Map: Table Name → Key Column Name
var tableKeys = new Dictionary<string, string>
{
{ "DimCustomer", "CustomerKey" },
{ "DimProduct", "ProductKey" },
{ "DimDate", "DateKey" },
{ "DimStore", "StoreKey" },
{ "DimEmployee", "EmployeeKey" }
};
// ============================================================================
// SCRIPT LOGIC
// ============================================================================
var updatedCount = 0;
foreach(var entry in tableKeys)
{
var tableName = entry.Key;
var keyColumnName = entry.Value;
if(!Model.Tables.Contains(tableName))
{
Info("⚠ Table not found: " + tableName);
continue;
}
var table = Model.Tables[tableName];
if(!table.Columns.Contains(keyColumnName))
{
Info("⚠ Column not found: " + tableName + "[" + keyColumnName + "]");
continue;
}
var column = table.Columns[keyColumnName];
column.IsKey = true;
column.IsHidden = true; // Best practice: hide key columns from report view
column.SummarizeBy = AggregateFunction.None;
updatedCount++;
Info("✓ Set as key: " + tableName + "[" + keyColumnName + "]");
}
Info("\nMarked " + updatedCount + " columns as keys");
// ============================================================================
// NOTES
// ============================================================================
Info("\nBEST PRACTICES:");
Info(" - IsKey = true automatically sets IsNullable = false");
Info(" - Key columns should typically be hidden from reports");
Info(" - Set SummarizeBy = None for key columns");
Info(" - Use for dimension table primary keys");
Info(" - Improves relationship and query performance");
/*
* Title: Set Column IsNullable Property
*
* Description: Controls whether columns can contain null values.
*
* WHEN TO USE:
* - Enforce data quality rules (required fields)
* - Document expected data constraints
* - Validate data integrity during refresh
* - Improve query optimization by declaring non-null columns
* - Support compliance and governance requirements
*
* NOTE: Key columns (IsKey = true) automatically set IsNullable = false
*
* Usage: Configure nullable settings below.
* CLI: te "workspace/model" set-is-nullable.csx --file
*
* Non-interactive: Yes
*/
// ============================================================================
// CONFIGURATION
// ============================================================================
var tableName = "Sales";
// Map: Column Name → IsNullable
var nullableSettings = new Dictionary<string, bool>
{
// Required fields (must not be null)
{ "OrderID", false },
{ "CustomerKey", false },
{ "ProductKey", false },
{ "OrderDate", false },
{ "Quantity", false },
{ "Revenue", false },
// Optional fields (can be null)
{ "ShipDate", true }, // May not be shipped yet
{ "Comments", true }, // Optional text field
{ "DiscountPercent", true }, // May have no discount
{ "PromotionCode", true } // Optional promotion
};
// ============================================================================
// SCRIPT LOGIC
// ============================================================================
var table = Model.Tables[tableName];
var updatedCount = 0;
foreach(var entry in nullableSettings)
{
var columnName = entry.Key;
var isNullable = entry.Value;
if(table.Columns.Contains(columnName))
{
var column = table.Columns[columnName];
// Check if this is a key column (can't override)
if(column.IsKey && isNullable)
{
Info("⚠ Cannot set IsNullable = true for key column: " + columnName);
continue;
}
column.IsNullable = isNullable;
updatedCount++;
var status = isNullable ? "nullable" : "NOT NULL";
Info("✓ " + columnName + " → " + status);
}
else
{
Info("⚠ Column not found: " + columnName);
}
}
Info("\nConfigured IsNullable for " + updatedCount + " columns in " + tableName);
// ============================================================================
// NOTES
// ============================================================================
Info("\nIMPORTANT:");
Info(" - IsNullable = false enforces NOT NULL constraint");
Info(" - IsNullable = true allows NULL values");
Info(" - Key columns (IsKey = true) are always non-nullable");
Info(" - Use for data quality validation during refresh");
Info(" - Helps document required vs optional fields");
/*
* Title: Set Column IsUnique Property
*
* Description: Marks columns that contain only unique values.
*
* WHEN TO USE:
* - Document data uniqueness constraints
* - Validate data quality (ensure no duplicates)
* - Improve query optimization for unique columns
* - Support data governance and constraint validation
* - Identify candidate keys or alternate identifiers
*
* Usage: Configure unique constraints below.
* CLI: te "workspace/model" set-is-unique.csx --file
*
* Non-interactive: Yes
*/
// ============================================================================
// CONFIGURATION
// ============================================================================
var tableName = "Customers";
// Columns that should contain unique values
var uniqueColumns = new[]
{
"CustomerKey", // Primary key
"CustomerID", // Alternate key
"EmailAddress", // Unique email
"TaxID", // Unique tax identifier
"AccountNumber" // Unique account number
};
// ============================================================================
// SCRIPT LOGIC
// ============================================================================
var table = Model.Tables[tableName];
var updatedCount = 0;
foreach(var columnName in uniqueColumns)
{
if(table.Columns.Contains(columnName))
{
table.Columns[columnName].IsUnique = true;
updatedCount++;
Info("✓ Marked as unique: " + columnName);
}
else
{
Info("⚠ Column not found: " + columnName);
}
}
Info("\nMarked " + updatedCount + " columns as unique in " + tableName);
// ============================================================================
// NOTES
// ============================================================================
Info("\nUSE CASES:");
Info(" - Primary and alternate keys");
Info(" - Email addresses");
Info(" - Social Security Numbers, Tax IDs");
Info(" - Account numbers, Customer IDs");
Info(" - License plate numbers, serial numbers");
Info("");
Info("IMPORTANT:");
Info(" - IsUnique is a constraint declaration, not enforcement");
Info(" - Use for documentation and validation");
Info(" - Helps optimize query performance");
/*
* Title: Set KeepUniqueRows Property
*
* Description: Controls whether grouping happens by key or by value for hierarchies.
*
* WHEN TO USE:
* - Configure hierarchy behavior for dimensional modeling
* - Control aggregation grouping in parent-child hierarchies
* - Optimize query performance for specific hierarchy patterns
* - Define whether duplicate values at different levels are treated separately
*
* TRUE: Group by key (each row is unique by key, even with duplicate values)
* FALSE: Group by value (rows with same value are grouped together)
*
* Usage: Configure KeepUniqueRows settings below.
* CLI: te "workspace/model" set-keep-unique-rows.csx --file
*
* Non-interactive: Yes
*/
// ============================================================================
// CONFIGURATION
// ============================================================================
var tableName = "DimEmployee";
// Map: Column Name → KeepUniqueRows
var keepUniqueRowsSettings = new Dictionary<string, bool>
{
// TRUE: Treat each row as unique (even if values duplicate)
{ "EmployeeKey", true }, // Primary key - always unique
{ "ManagerKey", true }, // Foreign key - keep separate instances
// FALSE: Group by value (duplicate values aggregate together)
{ "Department", false }, // Aggregate employees by department
{ "JobTitle", false }, // Aggregate employees by job title
{ "Location", false } // Aggregate employees by location
};
// ============================================================================
// SCRIPT LOGIC
// ============================================================================
var table = Model.Tables[tableName];
var updatedCount = 0;
foreach(var entry in keepUniqueRowsSettings)
{
var columnName = entry.Key;
var keepUniqueRows = entry.Value;
if(table.Columns.Contains(columnName))
{
table.Columns[columnName].KeepUniqueRows = keepUniqueRows;
updatedCount++;
var behavior = keepUniqueRows ? "by key (unique)" : "by value (aggregate)";
Info("✓ " + columnName + " → Group " + behavior);
}
else
{
Info("⚠ Column not found: " + columnName);
}
}
Info("\nConfigured KeepUniqueRows for " + updatedCount + " columns in " + tableName);
// ============================================================================
// NOTES
// ============================================================================
Info("\nKEEP UNIQUE ROWS:");
Info(" TRUE - Each row treated as unique (by key)");
Info(" Use for: Keys, IDs, unique identifiers");
Info(" FALSE - Rows with same value grouped together");
Info(" Use for: Categorical attributes, grouping columns");
Info("");
Info("HIERARCHY IMPACT:");
Info(" - Affects how hierarchies aggregate and display data");
Info(" - Important for parent-child and ragged hierarchies");
/*
* Title: Set Source Column Names
*
* Description: Sets the SourceColumn property on DataColumns. This specifies
* the name of the column from the data source that maps to this model column.
* Useful when renaming columns in the model while preserving source mapping.
*
* Note: This property only applies to DataColumn (not CalculatedColumn).
*
* Usage: Configure source column mappings below.
* CLI: te "workspace/model" set-source-column.csx --file
*
* Non-interactive: Yes
*/
// ============================================================================
// CONFIGURATION
// ============================================================================
var tableName = "Sales";
// Map: Model Column Name → Source Column Name
var sourceColumnMappings = new Dictionary<string, string>
{
// Common pattern: Rename to business-friendly names
{ "CustomerKey", "customer_id" },
{ "ProductKey", "product_id" },
{ "OrderDate", "order_date" },
{ "ShipDate", "ship_date" },
{ "Revenue", "total_revenue" },
{ "Quantity", "order_quantity" },
{ "UnitPrice", "unit_price" },
// Handle source columns with special characters
{ "DiscountPercent", "discount_pct" },
{ "IsActive", "is_active_flag" }
};
// ============================================================================
// SCRIPT LOGIC
// ============================================================================
var table = Model.Tables[tableName];
var updatedCount = 0;
var skippedCount = 0;
foreach(var entry in sourceColumnMappings)
{
var modelColumnName = entry.Key;
var sourceColumnName = entry.Value;
if(!table.Columns.Contains(modelColumnName))
{
Info("⚠ Column not found: " + modelColumnName);
continue;
}
var column = table.Columns[modelColumnName];
// Check if this is a DataColumn (not CalculatedColumn)
if(column is DataColumn)
{
var dataColumn = column as DataColumn;
dataColumn.SourceColumn = sourceColumnName;
updatedCount++;
Info("✓ " + modelColumnName + " → " + sourceColumnName);
}
else
{
Info("⚠ Skipped (not a DataColumn): " + modelColumnName + " (type: " + column.GetType().Name + ")");
skippedCount++;
}
}
Info("\nUpdated SourceColumn for " + updatedCount + " columns in " + tableName);
if(skippedCount > 0)
{
Info("Skipped " + skippedCount + " calculated columns (SourceColumn only applies to DataColumn)");
}
// ============================================================================
// NOTES
// ============================================================================
Info("\nIMPORTANT:");
Info("- SourceColumn must match the column name returned during refresh/processing");
Info("- Applies only to DataColumn (not CalculatedColumn, CalculatedTableColumn)");
Info("- Use when renaming model columns while preserving source mapping");
// Add Culture - NOT SUPPORTED IN TE2
Error("Adding cultures is not supported in Tabular Editor 2. Use Tabular Editor 3 or SSDT.");
// Delete Culture(s)
// Removes culture objects and their translations from the model
// ============================================================================
// CONFIGURATION
// ============================================================================
var deleteMode = "single"; // "single", "pattern", "all"
// Single mode
var cultureName = "es-ES";
// Pattern mode (wildcard matching)
var culturePattern = "es-*"; // Matches "es-ES", "es-MX", etc.
// Confirmation for "all" mode
var confirmDeleteAll = false;
// ============================================================================
// BUILD DELETE LIST
// ============================================================================
var culturesToDelete = new System.Collections.Generic.List<Culture>();
if (deleteMode == "single")
{
if (!Model.Cultures.Contains(cultureName))
{
Error("Culture not found: " + cultureName);
}
culturesToDelete.Add(Model.Cultures[cultureName]);
}
else if (deleteMode == "pattern")
{
var regex = "^" + culturePattern.Replace("*", ".*").Replace("?", ".") + "$";
culturesToDelete.AddRange(
Model.Cultures.Where(c =>
System.Text.RegularExpressions.Regex.IsMatch(c.Name, regex))
);
}
else if (deleteMode == "all")
{
if (!confirmDeleteAll)
{
Error("Safety check: Set confirmDeleteAll = true to delete all cultures");
}
culturesToDelete.AddRange(Model.Cultures);
}
else
{
Error("Invalid deleteMode: " + deleteMode);
}
if (culturesToDelete.Count == 0)
{
Error("No cultures found to delete");
}
// ============================================================================
// DELETE CULTURES
// ============================================================================
var deletedNames = new System.Collections.Generic.List<string>();
foreach (var culture in culturesToDelete.ToList())
{
deletedNames.Add(culture.Name);
culture.Delete();
}
// ============================================================================
// REPORT RESULTS
// ============================================================================
Info("Deleted Cultures\n" +
"================\n\n" +
"Mode: " + deleteMode + "\n" +
"Count: " + deletedNames.Count + "\n\n" +
"Deleted cultures:\n" +
string.Join("\n", deletedNames.Select(n => " - " + n)) + "\n\n" +
"Note: All translations for these cultures have been removed from the model.");
// List all Cultures and Translations
// Reports all cultures in the model and translation coverage
// ============================================================================
// CONFIGURATION
// ============================================================================
var showTranslationCoverage = true; // Show % of objects translated
var showSampleTranslations = false; // Show sample translated names (verbose)
var sampleCount = 5; // Number of sample translations to show
// ============================================================================
// BUILD CULTURE LIST
// ============================================================================
if (Model.Cultures.Count == 0)
{
Info("No cultures (translations) found in model");
return;
}
// ============================================================================
// BUILD REPORT
// ============================================================================
var report = new System.Text.StringBuilder();
report.AppendLine("Cultures and Translations");
report.AppendLine("=========================\n");
report.AppendLine("Total cultures: " + Model.Cultures.Count + "\n");
report.AppendLine(new string('=', 50) + "\n");
// List each culture
foreach (var culture in Model.Cultures)
{
report.AppendLine("Culture: " + culture.Name);
if (showTranslationCoverage)
{
// Count translated vs total objects
int totalTables = Model.Tables.Count;
int translatedTables = 0;
int totalMeasures = Model.AllMeasures.Count();
int translatedMeasures = 0;
int totalColumns = Model.AllColumns.Count();
int translatedColumns = 0;
int totalHierarchies = Model.AllHierarchies.Count();
int translatedHierarchies = 0;
foreach (var table in Model.Tables)
{
if (!string.IsNullOrWhiteSpace(table.TranslatedNames[culture]))
{
translatedTables++;
}
foreach (var measure in table.Measures)
{
if (!string.IsNullOrWhiteSpace(measure.TranslatedNames[culture]))
{
translatedMeasures++;
}
}
foreach (var column in table.Columns)
{
if (!string.IsNullOrWhiteSpace(column.TranslatedNames[culture]))
{
translatedColumns++;
}
}
foreach (var hierarchy in table.Hierarchies)
{
if (!string.IsNullOrWhiteSpace(hierarchy.TranslatedNames[culture]))
{
translatedHierarchies++;
}
}
}
report.AppendLine("\n Translation coverage:");
if (totalTables > 0)
{
var pct = (int)((translatedTables / (double)totalTables) * 100);
report.AppendLine(" Tables: " + translatedTables + "/" + totalTables + " (" + pct + "%)");
}
if (totalMeasures > 0)
{
var pct = (int)((translatedMeasures / (double)totalMeasures) * 100);
report.AppendLine(" Measures: " + translatedMeasures + "/" + totalMeasures + " (" + pct + "%)");
}
if (totalColumns > 0)
{
var pct = (int)((translatedColumns / (double)totalColumns) * 100);
report.AppendLine(" Columns: " + translatedColumns + "/" + totalColumns + " (" + pct + "%)");
}
if (totalHierarchies > 0)
{
var pct = (int)((translatedHierarchies / (double)totalHierarchies) * 100);
report.AppendLine(" Hierarchies: " + translatedHierarchies + "/" + totalHierarchies + " (" + pct + "%)");
}
}
if (showSampleTranslations)
{
var samples = new System.Collections.Generic.List<string>();
// Collect sample translations
foreach (var table in Model.Tables.Take(sampleCount))
{
var translated = table.TranslatedNames[culture];
if (!string.IsNullOrWhiteSpace(translated))
{
samples.Add("Table: " + table.Name + " → " + translated);
}
}
foreach (var measure in Model.AllMeasures.Take(sampleCount))
{
var translated = measure.TranslatedNames[culture];
if (!string.IsNullOrWhiteSpace(translated))
{
samples.Add("Measure: " + measure.Name + " → " + translated);
}
}
if (samples.Count > 0)
{
report.AppendLine("\n Sample translations:");
foreach (var sample in samples.Take(10))
{
report.AppendLine(" " + sample);
}
}
}
report.AppendLine("");
}
// ============================================================================
// REPORT RESULTS
// ============================================================================
Info(report.ToString());
// Modify Translations
// Add or update translated names and descriptions for objects
// ============================================================================
// CONFIGURATION
// ============================================================================
var cultureName = "es-ES";
// Translation mode
var translationMode = "specific"; // "specific", "table", "measure", "column"
// Specific translations (exact object references)
var tableTranslations = new Dictionary<string, string>()
{
{ "FactSales", "Ventas" },
{ "DimProduct", "Productos" },
{ "DimDate", "Fecha" }
};
var measureTranslations = new Dictionary<string, string>()
{
{ "Total Sales", "Ventas Totales" },
{ "Total Quantity", "Cantidad Total" }
};
var columnTranslations = new Dictionary<string, string>()
{
{ "DimProduct/ProductName", "Nombre del Producto" },
{ "DimDate/Year", "Año" },
{ "FactSales/Amount", "Importe" }
};
var hierarchyTranslations = new Dictionary<string, string>()
{
{ "DimDate/Calendar", "Calendario" }
};
// Also translate descriptions
var translateDescriptions = false;
var tableDescriptions = new Dictionary<string, string>()
{
{ "FactSales", "Tabla de hechos de ventas" }
};
// ============================================================================
// VALIDATION
// ============================================================================
if (!Model.Cultures.Contains(cultureName))
{
Error("Culture not found: " + cultureName + "\n\n" +
"Create culture first using add-culture.csx");
}
var culture = Model.Cultures[cultureName];
// ============================================================================
// APPLY TRANSLATIONS
// ============================================================================
int tablesTranslated = 0;
int measuresTranslated = 0;
int columnsTranslated = 0;
int hierarchiesTranslated = 0;
int descriptionsTranslated = 0;
// Translate tables
foreach (var kvp in tableTranslations)
{
if (Model.Tables.Contains(kvp.Key))
{
var table = Model.Tables[kvp.Key];
table.TranslatedNames[culture] = kvp.Value;
tablesTranslated++;
if (translateDescriptions && tableDescriptions.ContainsKey(kvp.Key))
{
table.TranslatedDescriptions[culture] = tableDescriptions[kvp.Key];
descriptionsTranslated++;
}
}
else
{
Info("Warning: Table not found: " + kvp.Key);
}
}
// Translate measures
foreach (var kvp in measureTranslations)
{
var measure = Model.AllMeasures.FirstOrDefault(m => m.Name == kvp.Key);
if (measure != null)
{
measure.TranslatedNames[culture] = kvp.Value;
measuresTranslated++;
}
else
{
Info("Warning: Measure not found: " + kvp.Key);
}
}
// Translate columns
foreach (var kvp in columnTranslations)
{
var parts = kvp.Key.Split('/');
if (parts.Length == 2 && Model.Tables.Contains(parts[0]))
{
var table = Model.Tables[parts[0]];
if (table.Columns.Contains(parts[1]))
{
table.Columns[parts[1]].TranslatedNames[culture] = kvp.Value;
columnsTranslated++;
}
else
{
Info("Warning: Column not found: " + kvp.Key);
}
}
else
{
Info("Warning: Invalid column path: " + kvp.Key);
}
}
// Translate hierarchies
foreach (var kvp in hierarchyTranslations)
{
var parts = kvp.Key.Split('/');
if (parts.Length == 2 && Model.Tables.Contains(parts[0]))
{
var table = Model.Tables[parts[0]];
if (table.Hierarchies.Contains(parts[1]))
{
table.Hierarchies[parts[1]].TranslatedNames[culture] = kvp.Value;
hierarchiesTranslated++;
}
else
{
Info("Warning: Hierarchy not found: " + kvp.Key);
}
}
else
{
Info("Warning: Invalid hierarchy path: " + kvp.Key);
}
}
// ============================================================================
// REPORT RESULTS
// ============================================================================
Info("Applied Translations\n" +
"====================\n\n" +
"Culture: " + cultureName + "\n\n" +
"Translations applied:\n" +
" Tables: " + tablesTranslated + "\n" +
" Measures: " + measuresTranslated + "\n" +
" Columns: " + columnsTranslated + "\n" +
" Hierarchies: " + hierarchiesTranslated + "\n" +
" Descriptions: " + descriptionsTranslated);
Cultures Scripts
Scripts for managing cultures and translations in Tabular models.
Available Scripts
add-culture.csx- Add a new culture to the modeldelete-culture.csx- Remove a culture from the modellist-cultures.csx- List all cultures in the modelmodify-translations.csx- Update translations for objects
Usage Examples
Execute Inline
te "model.bim" 'var culture = Model.AddCulture("es-ES"); culture.Name = "Spanish";'Execute Script File
te "model.bim" samples/cultures/add-culture.csx --file
te "Production/Sales" samples/cultures/modify-translations.csx --fileWith Fabric CLI Workflow
# Export model
fab export "Workspace/Model.SemanticModel" -o ./model -f
# Add cultures
te "./model/Model.SemanticModel/model.bim" samples/cultures/add-culture.csx --file
# Import back
fab import "Workspace/Model.SemanticModel" -i ./model/Model.SemanticModel -fCommon Patterns
Add Culture
// Add Spanish culture
var culture = Model.AddCulture("es-ES");
culture.Name = "Spanish";Set Translations
// Translate table name
var culture = Model.Cultures["es-ES"];
Model.Tables["Sales"].TranslatedNames[culture] = "Ventas";
// Translate column name
Model.Tables["Sales"].Columns["Amount"].TranslatedNames[culture] = "Cantidad";
// Translate measure
Model.Tables["Sales"].Measures["Total Sales"].TranslatedNames[culture] = "Ventas Totales";List All Cultures
foreach(var culture in Model.Cultures) {
Info("Culture: " + culture.Name + " (" + culture.LinguisticMetadata.Language + ")");
}Delete Culture
var culture = Model.Cultures["es-ES"];
if(culture != null) {
culture.Delete();
Info("Deleted culture: es-ES");
}Bulk Translate Objects
var culture = Model.Cultures["es-ES"];
// Translate all measures
foreach(var measure in Model.AllMeasures) {
// Example: append " (ES)" to measure names
measure.TranslatedNames[culture] = measure.Name + " (ES)";
}Property Reference
Culture Properties
Name- Culture name (e.g., "es-ES")LinguisticMetadata.Language- Language codeTranslatedNames- Translation dictionary
Translation Properties
TranslatedNames[culture]- Get/set translated nameTranslatedDescriptions[culture]- Get/set translated descriptionTranslatedDisplayFolders[culture]- Get/set translated display folder
Common Culture Codes
"en-US"- English (United States)"es-ES"- Spanish (Spain)"fr-FR"- French (France)"de-DE"- German (Germany)"it-IT"- Italian (Italy)"pt-BR"- Portuguese (Brazil)"ja-JP"- Japanese (Japan)"zh-CN"- Chinese (Simplified)"ko-KR"- Korean (Korea)"nl-NL"- Dutch (Netherlands)
Best Practices
1. Add Cultures First
- Add all required cultures before adding translations
- Use standard culture codes (e.g., "es-ES", not "spanish")
- Provide culture name for clarity
2. Systematic Translation
- Translate all visible objects
- Keep translations consistent
- Use professional translation services for production
3. Testing
- Test with each culture in Power BI
- Verify translations appear correctly
- Check for truncation issues
4. Maintenance
- Update translations when adding new objects
- Document translation keys
- Version control translation files
See Also
- Tables
- Measures
- Columns
// Add/Set Display Folder
// Assigns measures/columns to folder path. Supports nested folders with /
// Use: table="Sales" targetFolder="Revenue/Metrics" targetType="measures" selectionMethod="pattern"
// ============================================================================
// CONFIGURATION - Modify these values
// ============================================================================
// Display folder path (use forward slashes for nested folders)
// Example: "Sales/Key Metrics" creates Sales\Key Metrics hierarchy
var targetFolder = "Sales/Revenue Metrics";
// Target type: "measures" or "columns"
var targetType = "measures";
// Target selection method: "table", "pattern", "list"
var selectionMethod = "pattern";
// Method: "table" - Specify table name
var tableName = "Sales";
// Method: "pattern" - Specify name pattern (measures/columns matching this pattern)
var namePattern = "Revenue"; // Matches: "Total Revenue", "Revenue YTD", etc.
// Method: "list" - Specify exact names
var objectNames = new[] { "Total Revenue", "Revenue per Customer", "Revenue Growth %" };
// ============================================================================
// SCRIPT LOGIC
// ============================================================================
var updatedCount = 0;
if (targetType.ToLower() == "measures")
{
// Work with measures
var measures = new List<Measure>();
if (selectionMethod == "table" && Model.Tables.Contains(tableName))
{
measures = Model.Tables[tableName].Measures.ToList();
}
else if (selectionMethod == "pattern")
{
measures = Model.AllMeasures.Where(m => m.Name.Contains(namePattern)).ToList();
}
else if (selectionMethod == "list")
{
foreach(var name in objectNames)
{
foreach(var table in Model.Tables)
{
if(table.Measures.Contains(name))
{
measures.Add(table.Measures[name]);
break;
}
}
}
}
foreach(var measure in measures)
{
measure.DisplayFolder = targetFolder;
updatedCount++;
}
}
else if (targetType.ToLower() == "columns")
{
// Work with columns
var columns = new List<Column>();
if (selectionMethod == "table" && Model.Tables.Contains(tableName))
{
columns = Model.Tables[tableName].Columns.ToList();
}
else if (selectionMethod == "pattern")
{
columns = Model.AllColumns.Where(c => c.Name.Contains(namePattern)).ToList();
}
else if (selectionMethod == "list")
{
foreach(var name in objectNames)
{
foreach(var table in Model.Tables)
{
if(table.Columns.Contains(name))
{
columns.Add(table.Columns[name]);
break;
}
}
}
}
foreach(var column in columns)
{
column.DisplayFolder = targetFolder;
updatedCount++;
}
}
Info("Assigned " + updatedCount + " " + targetType + " to folder: " + targetFolder);
/*
* Title: Clear all display folders
*
* Author: Claude Tabular Editor Plugin
*
* Description: Removes all display folders from measures and columns across
* the entire model. Useful for resetting folder organization.
*
* Usage: Run this script on the entire model.
* CLI: te "workspace/model" script.csx --file
*
* Non-interactive: Yes (works on Model.Tables)
*/
var clearedCount = 0;
foreach(var table in Model.Tables) {
foreach(var measure in table.Measures) {
if(measure.DisplayFolder.Length > 0) {
measure.DisplayFolder = "";
clearedCount++;
}
}
foreach(var column in table.Columns) {
if(column.DisplayFolder.Length > 0) {
column.DisplayFolder = "";
clearedCount++;
}
}
}
Info("Cleared " + clearedCount + " display folders from " + Model.Tables.Count + " tables");
// Example: Clear All Display Folders
// This script removes all display folder assignments
// Clear measure folders
foreach(var measure in Model.AllMeasures) {
measure.DisplayFolder = "";
}
// Clear column folders
foreach(var table in Model.Tables) {
foreach(var column in table.Columns) {
column.DisplayFolder = "";
}
}
Info("Cleared all display folders");
// Example: Organize Columns by Semantic Type
// This script organizes columns into display folders by their semantic meaning
var invoices = Model.Tables["Invoices"];
// Keys
invoices.Columns["Invoice ID"].DisplayFolder = "Columns/Keys";
invoices.Columns["Customer Key"].DisplayFolder = "Columns/Keys";
invoices.Columns["Product Key"].DisplayFolder = "Columns/Keys";
// Dates
invoices.Columns["Invoice Date"].DisplayFolder = "Columns/Dates";
invoices.Columns["Due Date"].DisplayFolder = "Columns/Dates";
invoices.Columns["Ship Date"].DisplayFolder = "Columns/Dates";
// Metrics
invoices.Columns["Quantity"].DisplayFolder = "Columns/Metrics";
invoices.Columns["Unit Price"].DisplayFolder = "Columns/Metrics";
invoices.Columns["Total Amount"].DisplayFolder = "Columns/Metrics";
Info("Organized columns in Invoices table");
// Example: Organize Columns into Display Folders by Semantic Type
// This script organizes columns across multiple tables into logical folders
Info("Organizing columns into display folders...");
// Organize Invoices table
var invoices = Model.Tables["Invoices"];
// Keys
invoices.Columns["Invoice ID"].DisplayFolder = "Columns/Keys";
invoices.Columns["Customer Key"].DisplayFolder = "Columns/Keys";
invoices.Columns["Product Key"].DisplayFolder = "Columns/Keys";
invoices.Columns["Salesperson Key"].DisplayFolder = "Columns/Keys";
// Dates
invoices.Columns["Invoice Date"].DisplayFolder = "Columns/Dates";
invoices.Columns["Due Date"].DisplayFolder = "Columns/Dates";
invoices.Columns["Ship Date"].DisplayFolder = "Columns/Dates";
// Metrics
invoices.Columns["Net Invoice Value"].DisplayFolder = "Columns/Metrics";
invoices.Columns["Net Invoice Quantity"].DisplayFolder = "Columns/Metrics";
invoices.Columns["Unit Price"].DisplayFolder = "Columns/Metrics";
// Costs
invoices.Columns["Delivery Cost"].DisplayFolder = "Columns/Costs";
invoices.Columns["Freight"].DisplayFolder = "Columns/Costs";
Info("Organized Invoices table columns");
// Organize Customers table
var customers = Model.Tables["Customers"];
customers.Columns["Customer Key"].DisplayFolder = "Columns/Keys";
customers.Columns["Customer Name"].DisplayFolder = "Columns/Names";
customers.Columns["Account Name"].DisplayFolder = "Columns/Names";
customers.Columns["Type"].DisplayFolder = "Columns/Attributes";
customers.Columns["Category"].DisplayFolder = "Columns/Attributes";
Info("Organized Customers table columns");
// Organize Products table
var products = Model.Tables["Products"];
products.Columns["Product Key"].DisplayFolder = "Columns/Keys";
products.Columns["Product Name"].DisplayFolder = "Columns/Names";
products.Columns["Type"].DisplayFolder = "Columns/Attributes";
products.Columns["Subtype"].DisplayFolder = "Columns/Attributes";
products.Columns["Size"].DisplayFolder = "Columns/Specifications";
products.Columns["Weight"].DisplayFolder = "Columns/Specifications";
Info("Organized Products table columns");
Info("Column organization complete!");
/*
* Title: Organize measures by type into display folders
*
* Author: Tabular Editor Community
*
* Description: Organizes measures into display folders based on naming patterns.
* YTD, MTD, QTD measures go into "Time Intelligence", % measures go into "Ratios", etc.
*
* Usage: Run this script on the entire model or selected measures.
* CLI: te "workspace/model" script.csx --file
*
* Non-interactive: Yes (works on Model.AllMeasures)
*/
var organizedCount = 0;
foreach(var m in Model.AllMeasures) {
// Time Intelligence measures
if(m.Name.Contains(" YTD") || m.Name.Contains(" MTD") || m.Name.Contains(" QTD") ||
m.Name.Contains(" PY") || m.Name.Contains(" YoY")) {
m.DisplayFolder = "Time Intelligence";
organizedCount++;
}
// Percentage/Ratio measures
else if(m.Name.Contains("%") || m.Name.Contains("Percent") || m.Name.Contains("Rate")) {
m.DisplayFolder = "Ratios";
organizedCount++;
}
// Count measures
else if(m.Name.StartsWith("# ") || m.Name.Contains("Count")) {
m.DisplayFolder = "Counts";
organizedCount++;
}
// Average measures
else if(m.Name.Contains("Avg") || m.Name.Contains("Average")) {
m.DisplayFolder = "Averages";
organizedCount++;
}
// Sum measures
else if(m.Name.Contains("Sum") || m.Name.Contains("Total")) {
m.DisplayFolder = "Totals";
organizedCount++;
}
}
Info("Organized " + organizedCount + " measures into display folders");
Display Folders Scripts
Scripts for organizing objects into display folders in Tabular models.
Available Scripts
clear_all_display_folders.csx- Remove all display folders from the modelclear_display_folders.csx- Clear display folders for specific objectsorganize_columns_by_type.csx- Organize columns into folders by data typeorganize_folders.csx- Organize measures into logical folder structureorganize_measures_by_type.csx- Organize measures by type (Base, Time Intelligence, etc.)
Usage Examples
Execute Inline
te "model.bim" 'foreach(var m in Model.AllMeasures.Where(m => m.Name.StartsWith("Total"))) { m.DisplayFolder = "Totals"; }'Execute Script File
te "model.bim" samples/display-folders/organize_folders.csx --file
te "Production/Sales" samples/display-folders/organize_measures_by_type.csx --fileWith Fabric CLI Workflow
# Export model
fab export "Workspace/Model.SemanticModel" -o ./model -f
# Organize folders
te "./model/Model.SemanticModel/model.bim" samples/display-folders/organize_folders.csx --file
# Import back
fab import "Workspace/Model.SemanticModel" -i ./model/Model.SemanticModel -fCommon Patterns
Organize by Naming Pattern
// Organize measures with prefixes into folders
foreach(var measure in Model.AllMeasures) {
if(measure.Name.Contains(" - ")) {
var parts = measure.Name.Split(new[] { " - " }, 2, StringSplitOptions.None);
measure.DisplayFolder = parts[0];
}
}Create Nested Folders
// Create nested folder structure
foreach(var measure in Model.AllMeasures) {
if(measure.Name.StartsWith("YTD")) {
measure.DisplayFolder = "Time Intelligence\\Year to Date";
}
else if(measure.Name.StartsWith("MTD")) {
measure.DisplayFolder = "Time Intelligence\\Month to Date";
}
}Organize Columns by Type
foreach(var column in Model.AllColumns) {
if(column.DataType == DataType.DateTime) {
column.DisplayFolder = "Dates";
}
else if(column.DataType == DataType.String) {
column.DisplayFolder = "Attributes";
}
else if(column.DataType == DataType.Int64 || column.DataType == DataType.Double) {
column.DisplayFolder = "Numeric";
}
}Clear All Display Folders
foreach(var measure in Model.AllMeasures) {
measure.DisplayFolder = "";
}
foreach(var column in Model.AllColumns) {
column.DisplayFolder = "";
}Organize by Table
// Create folders based on table name
foreach(var measure in Model.AllMeasures) {
measure.DisplayFolder = measure.Table.Name + "\\Measures";
}Property Reference
Display Folder Properties
DisplayFolder- Folder path (use\\for nested folders)TranslatedDisplayFolders[culture]- Translated folder names
Folder Path Examples
"Sales"- Single folder"Sales\\Revenue"- Nested folder"Time Intelligence\\YTD"- Multi-level nesting""- No folder (root level)
Best Practices
1. Use Consistent Structure
- Define a standard folder hierarchy
- Use same structure across tables
- Document folder conventions
2. Nested Folders
- Use
\\separator for nested folders - Limit nesting depth to 2-3 levels
- Keep folder names short and clear
3. Naming Conventions
- Use Title Case for folder names
- Avoid special characters
- Keep names under 50 characters
4. Organization Patterns
- By subject area (Sales, Finance, HR)
- By calculation type (Base, Time Intelligence, KPIs)
- By data type (Dates, Attributes, Metrics)
Common Folder Structures
By Calculation Type
Base Measures
Calculated Measures
Time Intelligence
├─ YTD
├─ MTD
└─ QTD
KPIs
RatiosBy Subject Area
Sales
├─ Revenue
├─ Quantity
└─ Returns
Finance
├─ Costs
├─ Profit
└─ MarginBy Data Type
Dates
Attributes
Metrics
Keys (Hidden)See Also
- Measures
- Columns
- Bulk Operations
// Execute DAX queries from .dax files
// Pattern: File.ReadAllText("query.dax") then EvaluateDax()
var sampleQuery = @"
ADDCOLUMNS(
{""Table Count""},
""Value"",
" + Model.Tables.Count + @"
)";
var daxFilePath = @"sample-query.dax";
try {
System.IO.File.WriteAllText(daxFilePath, sampleQuery);
var daxFromFile = System.IO.File.ReadAllText(daxFilePath);
dynamic result = EvaluateDax(daxFromFile);
Info("EXECUTE FROM FILE:");
Info(" Result: " + result.Rows[0][1]);
System.IO.File.Delete(daxFilePath);
} catch (Exception ex) {
Error("Failed: " + ex.Message.Substring(0, Math.Min(100, ex.Message.Length)));
}
// Execute scalar DAX expressions
// Scalar expressions return simple values: Int64, String, Double, DateTime
Info("=== SCALAR DAX EXAMPLES ===\n");
// Arithmetic
Info("ARITHMETIC:");
Info(" 1 + 1 = " + EvaluateDax("1 + 1"));
Info(" 100 / 4 = " + EvaluateDax("100 / 4"));
Info(" POWER(2, 10) = " + EvaluateDax("POWER(2, 10)"));
// Text
Info("\nTEXT:");
Info(" CONCATENATE: " + EvaluateDax("CONCATENATE(\"DAX \", \"Rocks\")"));
Info(" UPPER: " + EvaluateDax("UPPER(\"tabular editor\")"));
// Dates
Info("\nDATE:");
Info(" TODAY() = " + EvaluateDax("TODAY()"));
Info(" DATE(2025, 1, 1) = " + EvaluateDax("DATE(2025, 1, 1)"));
// Logic
Info("\nLOGIC:");
Info(" IF: " + EvaluateDax("IF(10 > 5, \"Yes\", \"No\")"));
Info(" SWITCH: " + EvaluateDax("SWITCH(2, 1, \"One\", 2, \"Two\", 3, \"Three\")"));
// Requires data
try {
Info("\nAGGREGATE (requires data):");
Info(" COUNTROWS: " + EvaluateDax("COUNTROWS(" + Model.Tables.First().DaxObjectFullName + ")"));
} catch { }
// Format all calculated columns in the model
int _counter = 0;
foreach (var _column in Model.AllColumns)
{
if (Convert.ToString(_column.Type) == "Calculated")
{
(_column as CalculatedColumn).Expression = "\n" + FormatDax((_column as CalculatedColumn).Expression, shortFormat: true);
_counter++;
}
}
Info("Formatted " + Convert.ToString(_counter) + " calculated columns.");
// Format all DAX measures in the model
var _measures = Model.AllMeasures;
_measures.FormatDax();
Info("Formatted " + Convert.ToString(_measures.Count()) + " measures.");