How to Filter and Query Objects with LINQ
C# scripts use standard LINQ methods to filter, search and transform TOM object collections. These patterns are building blocks. Use collection-returning methods in foreach loops, bool-returning methods in if conditions and scalar-returning methods in variable assignments.
Quick reference
// Filter -- returns a collection for use in foreach or further chaining
Model.AllMeasures.Where(m => m.Name.EndsWith("Amount"));
// Find one -- returns a single object for assignment to a variable
var table = Model.Tables.First(t => t.Name == "Sales");
var tableOrNull = Model.Tables.FirstOrDefault(t => t.Name == "Sales");
// Existence checks -- returns bool for use in if conditions
if (table.Measures.Any(m => m.IsHidden)) { /* ... */ }
if (table.Columns.All(c => c.Description != "")) { /* ... */ }
// Count
var count = Model.AllColumns.Count(c => c.DataType == DataType.String);
// Project -- returns a List<string> of only the measure names
var names = Model.AllMeasures.Select(m => m.Name).ToList();
// Sort
var sorted = Model.AllMeasures.OrderBy(m => m.Name);
// Mutate
Model.AllMeasures.Where(m => m.FormatString == "").ForEach(m => m.FormatString = "0.00");
// Type filter
var calcCols = Model.AllColumns.OfType<CalculatedColumn>();
// Materialize before modifying the collection
table.Measures.Where(m => m.IsHidden).ToList().ForEach(m => m.Delete());
Filtering with Where
Where() returns all objects matching a predicate. Chain multiple conditions with && and ||.
// Columns with no description in a specific table
var undocumented = Model.Tables["Sales"].Columns
.Where(c => string.IsNullOrEmpty(c.Description));
Warning
String matching with Contains() finds the text anywhere in the expression, including inside string literals and comments. To detect actual DAX function usage, analyze the tokenized expression instead.
Tip
When checking expression content with Contains(), consider case-insensitive comparison: m.Expression.Contains("calculate", StringComparison.OrdinalIgnoreCase).
Finding a single object
First() returns the first match or throws if none exist. FirstOrDefault() returns null instead of throwing.
// Throws if "Sales" does not exist
var sales = Model.Tables.First(t => t.Name == "Sales");
// Returns null if not found (safe)
var table = Model.Tables.FirstOrDefault(t => t.Name == "Sales");
if (table == null) { Error("Table not found."); return; } // return exits the script
Existence and count checks
// Are all columns documented?
var allDocs = table.Columns.All(c => !string.IsNullOrEmpty(c.Description));
// How many string columns?
var count = Model.AllColumns.Count(c => c.DataType == DataType.String);
Projection with Select
Select() transforms each element. Use it to extract property values or build new structures.
// List of measure names only (returns List<string>)
var names = Model.AllMeasures.Select(m => m.Name).ToList();
// Table name + measure count pairs
var summary = Model.Tables.Select(t => new { t.Name, Count = t.Measures.Count() });
Mutation with ForEach
The ForEach() extension method applies an action to every element.
// Set format string on all currency measures
Model.AllMeasures
.Where(m => m.Name.EndsWith("Amount"))
.ForEach(m => m.FormatString = "#,##0.00");
// Move all measures in a table to a display folder
Model.Tables["Sales"].Measures.ForEach(m => m.DisplayFolder = "Sales Metrics");
Materializing before modifying a collection
When you modify objects inside a loop (delete, add, move), you change the collection being iterated. Always call .ToList() or .ToArray() first to create a snapshot.
// WRONG: modifying collection during iteration
table.Measures.Where(m => m.IsHidden).ForEach(m => m.Delete()); // throws
// CORRECT: materialize first, then modify
table.Measures.Where(m => m.IsHidden).ToList().ForEach(m => m.Delete());
Warning
Failing to materialize causes: "Collection was modified; enumeration operation may not complete." This applies to any modification, not just deletion.
Combining collections
Use Concat() to merge collections and Distinct() to remove duplicates.
// All hidden objects (measures + columns) in a table
var hidden = table.Measures.Where(m => m.IsHidden).Cast<ITabularNamedObject>()
.Concat(table.Columns.Where(c => c.IsHidden).Cast<ITabularNamedObject>());
// All unique tables referenced by selected measures
var tables = Selected.Measures
.Select(m => m.Table)
.Distinct();
Dynamic LINQ equivalent
In BPA rule expressions, the syntax differs from C# LINQ. Dynamic LINQ has no lambda arrows, uses keyword operators and compares enums as strings.
| C# LINQ (scripts) | Dynamic LINQ (BPA / Explorer filter) |
|---|---|
m.IsHidden |
IsHidden |
m.DataType == DataType.String |
DataType = "String" |
&& / \|\| / ! |
and / or / not |
== / != |
= / != or <> |
table.Columns.Count(c => c.IsHidden) |
Columns.Count(IsHidden) |
table.Measures.Any(m => m.IsHidden) |
Measures.Any(IsHidden) |
table.Columns.All(c => c.Description != "") |
Columns.All(Description != "") |
string.IsNullOrEmpty(m.Description) |
String.IsNullOrEmpty(Description) |
Note
Dynamic LINQ expressions evaluate against a single object in context. There is no equivalent to Model.AllMeasures or cross-table queries. Each BPA rule runs its expression once per object in its scope.