Extending Data Entities in D365 Finance & Operations Without Breaking Upgrades
Data entities are how almost everything moves in and out of Dynamics 365 Finance & Operations: the Data Management Framework (DMF), OData, dual-write, and Power Platform virtual entities all sit on top of them. Sooner or later you need a standard entity to carry one extra field, expose a related table, or enforce custom validation. Do it carelessly and your next One Version update overwrites the change — or worse, the entity silently stops importing. Here is how to extend data entities so they survive upgrades.
Extend, don't overlayer
Overlayering is gone. Every change to a standard entity is an extension: you create an extension element in your own model and the compiler merges it at build time. The rule of thumb:
- Adding fields, ranges, or relations to a standard entity → extension.
- Heavy reshaping (different staging, new natural key, denormalized joins) → a brand-new entity that you own outright.
A new entity is fully yours and never conflicts with Microsoft's changes. Extensions are lighter but constrained: you can add, you cannot remove or re-type what Microsoft ships.
Adding a field to a standard entity
Say you added PaymentHoldReason to VendTable and you need it on VendVendorV2Entity so integrations can read and write it. Three steps:
- Create an extension of the entity and drag the new field into the entity's data source so it is available for mapping.
- Add the entity field and map it to the data source column.
- Rebuild and regenerate the staging table.
The mapping is the part people forget. An entity field with no data source mapping compiles fine and silently returns null on every import.
// Run business logic as the entity writes back to the table. Put it on the
// ENTITY (not the table) so DMF and OData both honor it.
[ExtensionOf(tableStr(VendVendorV2Entity))]
final class VendVendorV2Entity_Extension
{
public void mapEntityToDataSource(DataEntityRuntimeContext _context)
{
next mapEntityToDataSource(_context);
VendTable vendTable = _context.getDatabaseEntityDataSourceObject(
tableNum(VendTable)) as VendTable;
if (this.PaymentHoldReason && !vendTable.PaymentStop)
{
vendTable.PaymentStop = NoYes::Yes;
}
}
}
Computed and virtual fields
Not every field maps one-to-one. Two escape hatches:
- Virtual (unmapped) fields are filled in code via
postLoad(). Use them for values assembled at read time — a formatted address, a derived status. - Computed columns push a SQL expression into the view that backs the entity. The database evaluates them, so they filter and sort efficiently, but the expression must be expressible through
SysComputedColumnserver methods.
private static server str compute formattedVendorName()
{
DataEntityName dataEntity = identifierStr(VendVendorV2Entity);
str account = SysComputedColumn::returnField(dataEntity,
dataEntityDataSourceStr(VendVendorV2Entity, VendTable),
fieldStr(VendTable, AccountNum));
str name = SysComputedColumn::returnField(dataEntity,
dataEntityDataSourceStr(VendVendorV2Entity, VendTable),
fieldStr(VendTable, VendorName));
return SysComputedColumn::stringConcat(account,
SysComputedColumn::literal(' - '), name);
}
Reach for postLoad() only when the database cannot do the work. Every virtual field is computed row by row in X++ after the result set returns, which destroys performance on bulk reads.
Validation that actually fires
validateField and validateWrite on the entity run for OData and DMF writes. Put cross-field rules here, not on the table, or imports bypass them entirely.
public boolean validateWrite()
{
boolean ret = next validateWrite();
if (this.PaymentHoldReason && !this.PaymentStop)
{
ret = checkFailed("A hold reason requires payment stop to be set.") && ret;
}
return ret;
}
The staging-table trap
DMF imports land in a staging table first, then copy to the target. When you add a field through an extension, the staging table does not grow automatically — you must regenerate it (right-click the entity, Generate staging table, or rebuild with that option set). Skip this and the import maps your field to nothing while reporting success.
| Symptom | Likely cause |
|---|---|
| New field always null after import | Field unmapped, or staging table not regenerated |
| Works in dev, fails in UAT | Staging table out of sync; redeploy the package |
| OData write ignores validation | Logic on the table instead of the entity |
| Exports slow after adding fields | Too many virtual fields doing postLoad work |
Cross-company and keys
Two more things bite teams:
- DataAreaId: public entities are cross-company by default. If your extension joins a company-specific table, set the entity's company behaviour explicitly or every caller has to pass
cross-company=true. - Entity keys: the natural key (for example
AccountNum) is what upsert matches on. If a new field belongs to identity, you almost always need a new entity — you cannot safely re-key Microsoft's.
Test like an integration, not a user
Before you ship: import a file through DMF and confirm the field round-trips; GET and PATCH the entity through OData with a REST client; and if the entity feeds dual-write or virtual entities, confirm change tracking still initializes after your change. An entity that passes the form but fails the API is the most common — and most expensive — way to learn this lesson in production.
Keep reading
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++.
Financial Dimensions in D365 Finance: How They Really Work
Default dimensions, ledger dimensions, account structures, and the DimensionAttributeValueCombination table explained — with X++ patterns for reading and building dimensions in D365 Finance.
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.