Extensible Data Security (XDS) in D365 Finance & Operations: Row-Level Security That Scales
Role-based security in Dynamics 365 Finance & Operations controls which objects a user can reach. It does not, on its own, control which rows. When a sales rep should only see their own region's customers, or a subcontractor should see only their own projects, you need Extensible Data Security (XDS) — policies that inject a WHERE clause into every query against a table, automatically and unavoidably. Here is how to build one that is correct and fast.
How XDS works
An XDS policy has three ingredients:
- A constrained table — the table whose rows you want to filter (for example
CustTable). - A primary table / policy query — a view that returns the rows the current user is allowed to see, based on their context.
- A context — what "the current user" means: a role, or an application-defined value you set in code.
At runtime, the kernel rewrites every query against the constrained table to join your policy query. The user literally cannot fetch rows the policy excludes — not through forms, not through OData, not through a report. That is the strength of XDS: it is enforced at the data layer, not bolted onto the UI.
A worked example: rep sees only their territory
Suppose CustTable has a SalesResponsible field, and you want each rep to see only customers they own.
- Create a query (
MyCustTerritoryPolicyQuery) overCustTablethat rangesSalesResponsibleto the current worker. - Create a security policy referencing that query, with
CustTableas the constrained table and ContextType = RoleName, tied to the "Sales rep" role. - The policy query uses a context function to resolve the current user to a worker:
-- conceptual shape of the policy query range
SELECT * FROM CustTable
WHERE CustTable.SalesResponsible =
(SELECT PersonnelNumber FROM HcmWorker
WHERE HcmWorker.Person = currentUserWorkerPerson())
In X++ the range is expressed with a computed column or a method on the policy that returns the constraint. The kernel appends it to every consuming query.
Application context for dynamic filters
Role-based context is static. When the filter depends on runtime data — "show only the company the user picked in a lookup" — use application context. You set a value in code and the policy reads it:
// Set the context for the session
XDSServices xds = new XDSServices();
xds.setContext('MyRegionContext', strFmt('%1', selectedRegionId));
Your policy's context type is then ContextString, and the policy query filters on that value. Application context is powerful but stateful — set it deliberately at the start of the relevant operation and clear it when done, or you will leak one user's filter into another scenario.
Performance: the part that bites
XDS adds a join to every query on the constrained table. If your policy query is expensive, you have just made the entire system slow.
| Pitfall | Fix |
|---|---|
| Policy query joins many tables | Flatten to a single indexed lookup; precompute if needed |
| Constraint column not indexed | Add an index covering the filter field |
| Policy applied to a hot, high-volume table | Constrain a narrower table or use a covering view |
| Context resolved with heavy code each call | Cache the resolved value per session |
The golden rule: the policy query must be cheap and indexable, because it runs implicitly on reads you never see in your own code.
Operational tips
- Scope tightly. Apply XDS to the smallest set of tables that achieves the requirement. Every constrained table is a permanent tax on queries.
- Mind integrations. OData and data entities honor XDS. An integration account that needs to see everything should not be in the constrained role — or you will spend a day debugging "missing" rows that the policy is hiding by design.
- Test the negative case. Confirm a user cannot see excluded rows through OData and exports, not just the form. XDS is enforced everywhere, which is exactly why you must verify everywhere.
- Document the context. Six months later, "why can't finance see these vouchers?" has one answer hidden in a policy. Keep a short register of active XDS policies and what each constrains.
XDS is the right tool when row visibility is a security requirement rather than a UI nicety. Build the policy query like it is on the hot path — because it is.
Keep reading
Extending Data Entities in D365 Finance & Operations Without Breaking Upgrades
Add fields, computed columns, and validation to standard D365 Finance & Operations data entities the upgrade-safe way — with X++ examples and the staging-table traps to avoid.
Chain of Command vs Event Handlers: Extending D365 F&O the Right Way
When to use Chain of Command and when to use pre/post event handlers in Dynamics 365 Finance & Operations — with X++ examples, a decision table, and the gotchas that trip up teams.
Electronic Reporting in D365 Finance: Building Custom Formats Without Code
A practical guide to the Electronic Reporting (ER) framework in D365 Finance — data models, model mappings, and format configurations to produce custom files without X++.
Newsletter
New posts, straight to your inbox
One email per post. No spam, no tracking pixels, unsubscribe anytime.
Comments
No comments yet. Be the first.