Views, Queries, and Maps
Build reusable data retrieval layers in D365 F&O β AOT views as virtual tables, AOT queries as reusable data sources, and maps as abstraction over similar tables. Master the data abstraction tools tested on MB-500.
Three data abstraction tools
D365 F&O gives you three AOT objects for working with data above the raw table level: views, queries, and maps. Each solves a different problem.
Think of a library.
A table is a bookshelf β it holds the actual books. A view is a reading list that pulls books from multiple shelves and shows them in one list. The books never move β the list just points to them. You can read the list but canβt rearrange the actual shelves through it.
A query is a search template saved in the catalogue system β βfiction, published after 2020, English only.β Anyone can use this saved search instead of writing their own, and they can add extra filters on top.
A map is like a universal borrowing form that works for both books and DVDs. The form has a βTitleβ field that maps to βBook Titleβ on the book system and βDVD Titleβ on the DVD system. Same form, different underlying sources.
AOT Views
A view is a read-only virtual table defined by a query. It exists in the AOT, compiles to a SQL Server view, and can be used anywhere youβd use a table for reading data.
When to use views
| Scenario | Why a View Helps |
|---|---|
| Joining multiple tables for a report | One data source instead of complex joins in every report |
| Aggregating data (SUM, COUNT, AVG) | Computed at database level β faster than X++ loops |
| Simplifying form data sources | Form binds to one view instead of juggling multiple tables |
| Backing a data entity | View serves as the query layer behind an entity |
Creating a view in Visual Studio
- Right-click project β Add New Item β Data Dictionary β View
- Set the view name (e.g.,
AxionInspectionSummaryView) - Expand Data Sources β add the base table and joined tables
- Define Fields β select which columns to expose
- Add Computed Columns for calculations or CASE expressions
- Set Group By fields for aggregation
Computed columns
Computed columns let you add logic that runs as SQL:
// Computed column: StatusText on AxionInspectionSummaryView
// Returns a human-readable status based on the enum value
private static server str statusText()
{
DictEnum dictEnum = new DictEnum(enumNum(AxionInspectionStatus));
str viewName = tableStr(AxionInspectionSummaryView);
// Use SysComputedColumn helper methods
str statusField = SysComputedColumn::comparisonField(
viewName,
identifierStr(AxionInspectionTable),
fieldStr(AxionInspectionTable, Status)
);
str result = SysComputedColumn::switch(
SysComputedColumn::cases(
SysComputedColumn::caseExpr(
SysComputedColumn::equalExpression(statusField, SysComputedColumn::returnLiteral(0)),
SysComputedColumn::returnLiteral("Open")
),
SysComputedColumn::caseExpr(
SysComputedColumn::equalExpression(statusField, SysComputedColumn::returnLiteral(1)),
SysComputedColumn::returnLiteral("In Progress")
),
SysComputedColumn::caseExpr(
SysComputedColumn::equalExpression(statusField, SysComputedColumn::returnLiteral(2)),
SysComputedColumn::returnLiteral("Completed")
)
),
SysComputedColumn::returnLiteral("Unknown")
);
return result;
}
Scenario: Vik builds an inspection dashboard view
PM Fatima needs a dashboard showing inspection counts by status and inspector. Instead of writing complex X++ aggregation code, Vik creates an AOT view:
- Data source:
AxionInspectionTablejoined toHcmWorker - Fields: InspectorName (from HcmWorker), Status
- Group By: InspectorName, Status
- Computed column: InspectionCount using
SysComputedColumn::count()
The view runs as a single SQL query β far faster than selecting all inspections and looping in X++. Fatimaβs dashboard form just binds to this view as its data source.
AOT Queries
An AOT query is a reusable, metadata-defined data retrieval object. Think of it as a saved SELECT statement that anyone can reference, extend, and compose with.
Queries vs views
| Feature | AOT View | AOT Query |
|---|---|---|
| Compiled to | SQL Server view | Runtime query object (translated to SQL at execution) |
| Read/Write | Read-only | Can be used for both read and write operations |
| Aggregation | Yes β GROUP BY, SUM, COUNT in SQL | Limited β primarily for row retrieval |
| Used as form data source | Yes β like a table | Yes β QueryBuildDataSource |
| Modifiable at runtime | No β fixed at compile time | Yes β add ranges, data sources, and sorting in code |
| Extensible | Via view extension | Via query extension |
| Performance | SQL-level optimization | Query plan varies based on runtime modifications |
Query structure
// AOT Query: AxionOpenInspections
// Data Sources:
// AxionInspectionTable
// Ranges:
// Status == Open (AxionInspectionStatus::Open)
// Sorting:
// InspectionDate (Descending)
// Joined Data Sources:
// HcmWorker (join on InspectorWorker == RecId)
// Fields: Name
Using an AOT query in X++
// Using the AOT query as a starting point, then adding a filter
Query query = new Query(queryStr(AxionOpenInspections));
QueryBuildDataSource qbds = query.dataSourceTable(tableNum(AxionInspectionTable));
// Add an extra range at runtime
QueryBuildRange range = qbds.addRange(fieldNum(AxionInspectionTable, InspectionDate));
range.value(queryValue(today()));
QueryRun queryRun = new QueryRun(query);
while (queryRun.next())
{
AxionInspectionTable inspection = queryRun.get(tableNum(AxionInspectionTable));
info(strFmt("Inspection %1 on %2", inspection.InspectionId, inspection.InspectionDate));
}
Exam tip: QueryBuildRange values
Range values in AOT queries support special syntax:
- Exact value:
"Open"orqueryValue(dateValue) - Range:
"1..100"β between 1 and 100 - Not equal:
"!Open"β everything except Open - Wildcard:
"INS*"β starts with INS - Or:
"Open,InProgress"β matches either value - Empty:
""β no filter (shows all)
The exam may ask which syntax returns the correct data set.
Maps
Maps are an abstraction layer that lets you write code that works against multiple tables with similar structures, without knowing at compile time which table youβre using.
Think of a universal remote control.
Your living room has a Samsung TV, a Sony sound bar, and an LG Blu-ray player. A universal remote has one βVolume Upβ button that works on all three devices β but behind the scenes, it sends different signals to each brand.
A map is like that universal remote. It has field names like βAccountNumβ and βNameβ that map to real fields on different tables β SalesTable might have βCustAccountβ while PurchTable has βVendAccount.β Your code talks to the mapβs βAccountNum,β and the map translates to the right field on whichever table is active.
Map structure
// Map: AxionDocumentMap
// Fields:
// DocumentId β maps to SalesTable.SalesId AND PurchTable.PurchId
// AccountNum β maps to SalesTable.CustAccount AND PurchTable.OrderAccount
// DocumentDate β maps to SalesTable.CreatedDateTime AND PurchTable.CreatedDateTime
//
// Mapped tables:
// SalesTable
// PurchTable
Using a map in X++
// A method that works on either sales or purchase documents
public void printDocumentInfo(AxionDocumentMap _documentMap)
{
info(strFmt("Document: %1, Account: %2, Date: %3",
_documentMap.DocumentId,
_documentMap.AccountNum,
_documentMap.DocumentDate));
}
// Calling with a SalesTable record β the map resolves automatically
SalesTable salesOrder;
select firstonly salesOrder;
AxionDocumentMap docMap;
docMap = salesOrder; // implicit mapping
this.printDocumentInfo(docMap);
Scenario: Sophie encounters a legacy map
Sophie Chen at Ferris Industries is debugging a report that shows βDocument Numberβ for both sales and purchase orders. The code uses AddressMap to get a single postal address regardless of whether the source is a customer, vendor, or warehouse address table.
Mentor Carl explains: βMaps were the AX 2012 way to write polymorphic code over tables. Today weβd use an interface, but a lot of standard code still uses maps. Donβt create new maps β but you need to understand existing ones.β
Choosing between views, queries, and maps
| Need | Best Choice | Why |
|---|---|---|
| Read-only aggregation (SUM, COUNT) | View | Compiled to SQL β runs at database level, fastest for aggregation |
| Reusable WHERE clause across forms | Query | Define ranges once, reuse everywhere, modify at runtime |
| Backing data source for a data entity | View | Entities often need joined/flattened data β views provide this cleanly |
| Polymorphic code over similar tables | Interface (or map if legacy) | Interfaces are extensible and modern; maps still work but can't be extended |
| Dynamic filtering on a list page | Query | User filters modify the query at runtime β views can't accept runtime parameters |
| Flat join of 3+ tables for export | View | Single virtual table simplifies the export mapping |
Vik needs to create a dashboard that shows the total number of inspections per status per month. The data comes from AxionInspectionTable. Which AOT object should he use?
Sophie needs to build a list page that shows open purchase orders. Users should be able to add their own filters at runtime (by vendor, date range, etc.). The base filter is 'Status = Open'. Which approach is correct?
A developer creates a new map in D365 F&O to abstract over SalesTable and PurchTable for a reporting module. A colleague suggests using an interface instead. Which statement is correct?
Next up: Building Data Entities β create OData-enabled data entities for import, export, and integrations.