Skip to content

Selectors

How the first part of every rule query determines what is checked.

The Selector is the first part of a rule — the from clause or the initial collection access. It determines which kind of code element the rule operates on.

Top-Level Collections

You can use any of these directly as a starting point:

Collection Element type Description
Types TypeModel All classes, structs, interfaces, records, enums
Methods MethodModel All methods; constructors are not included — reach them via t.Constructors on a type
Properties PropertyModel All properties
Fields FieldModel All fields
Events EventModel All events
Namespaces NamespaceModel All namespaces
Assemblies AssemblyModel All analyzed assemblies
TypeDependencies TypeDependency Coupling relationships between types
Files FileModel One per source file: using directives and comment trivia

Two Syntax Styles

The DSL supports both from x in Y (query syntax) and Y.Where(x => ...) (method syntax). Both produce the same result, with one difference in error behavior: if evaluating an element fails (for example a mistyped property), query syntax skips only that element, while method syntax aborts the whole rule and reports no violations.

Query syntax:

from t in Types
where t.Kind == "Class"
where t.IsAbstract
select t

Method syntax:

Types.Where(t => t.Kind == "Class" && t.IsAbstract)

When to use which? With multiple where clauses, query syntax is more readable. With a single condition or an aggregation (.Count > 10, where Count is a property), method syntax is shorter.

Per-Element Properties

Each element type has properties you can use in where clauses and sub-queries.

TypeModel (excerpt)

t.Name                 // simple name
t.FullName             // with namespace
t.Namespace            // namespace object; use t.Namespace.FullName for the name
t.Kind                 // "Class", "Interface", "Struct", "Enum", "Record"
t.IsAbstract           // bool
t.IsSealed             // bool
t.IsStatic             // bool
t.IsRecord             // bool
t.LinesOfCode          // int
t.NumberOfMethods      // int
t.Methods              // collection of MethodModel
t.Constructors         // collection of MethodModel
t.NestedTypes          // collection of TypeModel
t.DerivedTypes         // collection of TypeModel
t.UsedTypes            // which types does this type reference
t.UsedByTypes          // which types reference this one
t.BaseType.FullName    // parent class

MethodModel (excerpt)

m.Name                 // method name
m.AccessModifier       // "Public", "Private", "Protected", "Internal"
m.IsAsync              // bool
m.IsStatic             // bool
m.IsOverride           // bool
m.IsInterfaceImplementation // bool
m.Parameters           // collection of ParameterModel
m.CalledMethods        // which methods does this one call
m.CalledByMethods      // which methods call this one
m.LinesOfCode          // int
m.DeclaringType        // TypeModel

The full property and Kind reference lives in the Predicate catalog; when using an AI assistant, the get_authoring_docs MCP tool (topic predicates) returns the same reference.

Composition with Any / All / Count

Sub-queries on collection properties. For Any / All you'll usually pass a lambda (Any(m => ...)), but a zero-argument call like t.Methods.Any() is also valid and simply checks whether the collection has any elements. Count is available both as a property (t.Methods.Count) and as a call (t.Methods.Count()).

// Classes with at least one async method without CancellationToken
from t in Types
where t.Methods.Any(m =>
    m.IsAsync &&
    !m.Parameters.Any(p => p.TypeShortName == "CancellationToken"))
select t
// Classes with more than 10 public methods
from t in Types
where t.Methods.Where(m => m.AccessModifier == "Public").Count > 10
select t

Statement-Level Facts as a Selector

Some patterns live inside a method or constructor body — a catch clause, a member access, a literal, a binary expression. These are exposed as typed facts on every body (m.Catches, m.MemberAccesses, m.Literals, m.BinaryExpressions, m.Invocations, m.LocalDeclarations), reachable from the flattened AllBodies collection so a rule sees every method, constructor, and property accessor across every type without a nesting boundary:

@name "Direct DateTime usage"
@severity warn
@category "Testability"

AllBodies.SelectMany(m => m.MemberAccesses.Where(a => (a.Name == "Now" || a.Name == "UtcNow")
    && (a.ResolvedType.FullName == "System.DateTime" || a.ResolvedType.FullName == "System.DateTimeOffset")))

The full member catalog per fact type lives in the Predicate catalog.

What's Next