FetchXML query builder

<FetchXmlBuilder> edits a FetchXML query without anyone writing FetchXML. It covers the parts of the language people actually reach for — the table, the columns it returns, how the rows are sorted, and a filter tree of conditions and nested groups — and it joins related tables, aggregates, and runs what it built.

Try it

The real component against this environment’s metadata. Pick a table, add a column or a condition, join a related table, and watch the FetchXML panel keep up — then press Run to see what the query matches.

React example Blazor example React TypeScript Razor
Query Options FetchXML
Table
Account (account)Contact (contact)Opportunity (opportunity) The table the query reads from. Changing it clears the columns and sort order, which name columns the new table does not have.
Columns
Edit columns
Email (emailaddress1)
Full Name (fullname)
Sort
Edit sorts
Full Name (fullname) AscendingDescending
Filters
Start from a view Add filter Add related entity
No filters — every row is returned.
Maximum rows to return. Leave blank for no limit. Asks Dataverse how many rows match, alongside the page it returns — what a pager needs to show a total. The count stops at 5,000: past that the response reports that the limit was exceeded rather than how many there are. Breaks the query into parts and reassembles the results. Worth trying on a query with many joins or many lookup and computed columns — it can make a simple query slower. Passed to SQL Server. Only apply these when Microsoft support recommends one: the wrong hint can badly damage a query’s performance.
Paste or edit the FetchXML here. It is read a moment after you stop typing, and the editor above rebuilds itself from it.
Results
Run query

Not the same as FetchXMLBuilder

This page is about the component. FetchXMLBuilder is the C# class that composes the same XML in code, for a server-side caller building a query by hand. They produce the same thing and neither needs the other.

Metadata is required, not optional

Every table, column and relationship is chosen from what the environment actually has. That is deliberate: a logical name typed by hand produces FetchXML that parses perfectly and then fails against Dataverse, which is the worst kind of error to hand somebody. The usePowerPortalsProFetchXmlMetadata hook wires the builder to the framework’s cached metadata endpoints, so the pickers share whatever the grids on the same page have already fetched.

Blazor needs no wiring: <FetchXmlBuilder> resolves the portal's metadata service itself and reads the same cached endpoints, so its pickers also share whatever the grids on the page have already fetched.

The query round-trips

The builder parses FetchXML into a document model and writes it back out. Anything it has no editor for — an attribute from a newer platform version, an element written by another tool — is carried through untouched rather than dropped on load, so opening a query in the editor and saving it never quietly costs you part of it.

What that means for a saved query

Formatting is not preserved. Indentation and attribute quoting are normalized on the way out, so a query re-saved through the builder is the same query rather than the same text.

Supplying a query

There are three ways in, and all three go through the same reader:

  • The value prop. Supply it with onChange for a controlled component, or use defaultValue and let the builder own its state.
  • The FetchXML panel. Paste or type a query and the editor rebuilds itself from it about a second after you stop typing.
  • A saved view. Start from a view in the Filters header lists the table’s views and takes the whole query from the one you pick. It is a copy — nothing is written back to the view.

Whatever arrives is checked first. Malformed XML is reported with its line and column, and so is anything that would make the editor show something other than what was written — an operator or link type it does not know, a query naming no table. Those are refused outright and the query already on screen is left alone. Everything else is reported and still applied: a misspelled element or attribute, two tables sharing an alias, a condition reading from an alias nothing declares.

A join is added from the relationship picker, so the from and to columns come from the relationship rather than from memory. What a join can then contribute depends on its type:

  • Returning types — inner, outer and matchfirstrowusingcrossapply — bring the related table’s columns back, so their columns can be projected, sorted by and compared against.
  • Filtering types — exists, in, any, not any and all — return the row at most once and none of the related columns. The editor drops the alias when you switch to one, so the name is free for another join.
  • Has no related row is a preset rather than a link type: FetchXML has none for it, so the editor writes the outer join and the null test on the joined key that the platform expects, and keeps the two in step.

About “all”

FetchXML’s all does not mean what its name suggests — it matches rows that have related rows of which none satisfy the filter, and not all is documented as equivalent to any. Each type is labeled by what it does rather than what it is called, and an Invert conditions action on a filter group expresses the reading people usually want.

Working with the filter tree

A filter is a group of conditions combined with and or or, and groups nest inside one another to whatever depth the query needs. Four things in the tree are worth knowing about, because each answers a question FetchXML makes awkward to ask:

  • Invert replaces every condition in a group with its opposite and swaps and for or. It exists for one link type: all does not mean “every related row matches” — it matches rows whose related rows none satisfy the filter — so expressing the intuitive reading means inverting first. It is offered only where every condition below has a true opposite; most date operators have none, and inverting part of a filter would look inverted and match the wrong rows.
  • Compare to a column tests one column against another rather than against a value — FetchXML’s valueof. Six operators allow it and the platform refuses the rest outright, so the operator list narrows while the row is in that mode. Candidates come from every joined table, since a condition reaches a related column by naming the join’s alias.
  • Union hint runs an or group as a union of separate queries instead of one query with an OR across tables — a shape SQL cannot index well. Worth setting when the conditions in the group test columns in different related tables. A query may carry only one.
  • Start from a view copies a saved view’s query into the editor — its table, columns, sorts and conditions. Nothing is written back to the view itself.

Rows that are not finished yet

A condition missing the value its operator needs is outlined, as is an alias that is required and blank, or shared with another column. All three produce FetchXML that parses perfectly and then fails — or silently returns the wrong rows — so the editor says so rather than leaving it to be found when the query runs. Operators that need no value, such as Contains data and Today, are complete on their own and are never marked.

Aggregate queries

Turn on Aggregate in the Columns section and each column says what it contributes — a value computed across the rows, or the thing they are grouped by. The functions offered narrow to the column’s type, aliases become required and are filled in for you, a grouped date column gains a date part, and sorting switches to the aliases, because an aggregate query’s rows are groups and computed values with no columns to sort by.

allowAggregate={false} withdraws the switch, for a host that reads rows rather than totals — a rule evaluated one record at a time, a saved filter, anywhere a single row of computed values would not be an answer. It is absent rather than disabled, because no change to the query would bring it back. The one exception is a query that already aggregates: it keeps its controls however the prop is set, so one pasted into the FetchXML panel can still be edited back into an ordinary query.

AllowAggregate="false" does the same on Blazor, with the same exception: a query that already aggregates keeps its controls, so one pasted into the FetchXML tab can still be edited back into an ordinary query.

Mounting part of it

Every section can be switched off, and the sections are exported individually for a layout that needs them somewhere else — mount <FetchXmlBuilderProvider> and place the FetchXml*Section components yourself. Omitting a section removes its UI, never the underlying FetchXML: a query’s columns survive untouched with columns: false.

Blazor takes the same set as a Sections record, and the sections are components in their own right — cascade a FetchXmlBuilderState and place the FetchXml*Section components wherever a layout needs them. Hiding one removes its editor and never the query behind it.

Restricting the tables

allowedTables narrows the table picker to the logical names you list — for a surface that has no business querying the whole environment, like a report builder over the handful of tables a portal exposes. Omit it and every table is offered. It narrows what can be chosen, not what can be shown: a query that already names a table outside the list keeps it rather than being silently rewritten, and a related table is still named by its display name wherever a join mentions it. Joins themselves are not restricted, because they follow relationships — a property of the table already chosen.

AllowedTables is the Blazor equivalent, and narrows what can be chosen rather than what can be shown in exactly the same way.

Running the query

Supply renderResults and the builder grows a Results panel with a Run button, and decides when it may be pressed: once the query names a table and its columns, and no field is marked in red. Displaying rows takes a data client the builder deliberately has none of, so what a result looks like is yours — on a portal that usually means the framework’s own <MainGrid> over the query it hands back. Leave the prop off and the panel is not there.

On Blazor the same panel appears once a RenderResults fragment is supplied, and the runnable query it hands back carries the table, the FetchXML and the names the columns come back under. Leave the fragment out and there is no panel.

API

React Blazor

FetchXmlBuilder Class

Parameters

Name
Type
Default
Description
AllowAggregatebool
True
Whether the query may aggregate. Hides the offer when it may not.
AllowedTablesIReadOnlyList<string>?
The only tables the picker offers, or nothing for all of them.
FetchXmlstring?
The query being edited.
ReadOnlybool
False
Shows the query without offering to change it.
RenderResultsRenderFragment<FetchXmlRunnableQuery>?
Draws the rows the query returned, which also adds the results section.
SectionsFetchXmlBuilderSections
Which sections to show. Everything but the results, which needs somewhere to draw them.
ShowLogicalNamesbool
True
Whether tables and columns are named with their logical names beside their display names — Account Name (name) — and joins show the aliases their columns come back under. On by default; set it to false for users who know the data by its display names alone, to whom the logical names mean nothing.
Tablestring?
Pins the query to one table, so the table picker is shown but disabled.
Name: AllowAggregate
Type: bool
Default: True
Description: Whether the query may aggregate. Hides the offer when it may not.
Name: AllowedTables
Type: IReadOnlyList<string>?
Description: The only tables the picker offers, or nothing for all of them.
Name: FetchXml
Type: string?
Description: The query being edited.
Name: ReadOnly
Type: bool
Default: False
Description: Shows the query without offering to change it.
Name: RenderResults
Type: RenderFragment<FetchXmlRunnableQuery>?
Description: Draws the rows the query returned, which also adds the results section.
Name: Sections
Type: FetchXmlBuilderSections
Description: Which sections to show. Everything but the results, which needs somewhere to draw them.
Name: ShowLogicalNames
Type: bool
Default: True
Description: Whether tables and columns are named with their logical names beside their display names — Account Name (name) — and joins show the aliases their columns come back under. On by default; set it to false for users who know the data by its display names alone, to whom the logical names mean nothing.
Name: Table
Type: string?
Description: Pins the query to one table, so the table picker is shown but disabled.

Events

Name
Type
Description
FetchXmlChangedEventCallback<string>
Raised whenever an edit changes the query.
Name: FetchXmlChanged
Type: EventCallback<string>
Description: Raised whenever an edit changes the query.