Dataverse virtual entities are a platform-level feature of Microsoft Dataverse that project external data as if it were native Dataverse tables. The external data is never copied — every Create, Retrieve, RetrieveMultiple, Update, and Delete operation is translated into a call to the external data source in real time. [src1]
For Finance & Operations (F&O) apps specifically, Microsoft provides a dedicated virtual entity provider that maps all OData-enabled F&O entities into Dataverse. This is the primary mechanism for cross-system federation between customer engagement apps (Sales, Service, Marketing) and F&O apps (Finance, Supply Chain Management, Human Resources). [src2]
| Property | Value |
|---|---|
| Vendor | Microsoft |
| System | Microsoft Dataverse + Dynamics 365 Finance & Operations |
| API Surface | OData v4 Data Provider (built-in), Azure Cosmos DB (Marketplace), Custom Data Providers (.NET plug-ins), F&O Virtual Entity Provider |
| Current Version | Dataverse 2024 Wave 2+; F&O 10.0.12+ |
| Editions Covered | All licensed Power Platform environments; F&O: Finance, Supply Chain, HR, Commerce |
| Deployment | Cloud (on-premises excluded from service protection) |
| API Docs | Virtual Tables (Dataverse) |
| Status | GA |
Virtual entities operate through data providers that translate Dataverse operations into external system calls. Three provider types are available plus a dedicated F&O provider. [src1, src4, src8]
| Data Provider | Protocol | Best For | CRUD Support | Real-time? | External Auth | Notes |
|---|---|---|---|---|---|---|
| OData v4 (built-in) | HTTPS/OData | Any OData v4 endpoint | Full CRUD | Yes | Header/query string params | Default provider; maps Edm types to Dataverse types |
| Azure Cosmos DB | HTTPS/REST | Document databases | Full CRUD | Yes | Connection string | Azure Marketplace |
| Custom Data Provider | .NET Plug-ins | Any data source | Full CRUD via plug-in | Yes | Developer-defined | Register plug-ins for Create, Retrieve, RetrieveMultiple, Update, Delete |
| F&O Provider | SSL/TLS 1.2 S2S | Finance & Operations OData entities | Full CRUD | Yes | Microsoft Entra S2S | CDSVirtualEntityAdapterService translates queries; F&O business logic invoked |
| Limit Type | Value | Applies To | Notes |
|---|---|---|---|
| Column selection | All columns returned | Dataverse virtual tables | $select not applied server-side — all attributes returned [src1] |
| FK expansion latency | Significant per navigation property | Dataverse virtual tables | Each FK column triggers additional round-trip [src6] |
| OData timeout | Configurable (default 30s) | OData v4 Data Provider | Set in data source configuration [src4] |
| F&O latency overhead | <30ms per call (co-located) | F&O virtual entities | Same Azure region required [src2] |
| Limit Type | Value | Window | Notes |
|---|---|---|---|
| Requests per user per web server | 6,000 | 5-min sliding window | Per throttling key (user + app ID) [src5] |
| Combined execution time per user | 1,200 seconds (20 min) | 5-min sliding window | Across all requests for the user [src5] |
| Concurrent requests per user | 52 | Per web server | Multiple web servers in production [src5] |
| Resource-based throttling | Dynamic | Continuous | Triggered by high CPU/memory across all integrations [src5] |
Important: F&O virtual entity calls are exempt from F&O service protection API limits when Power Platform integration is enabled. However, Dataverse service protection API limits still apply. [src5]
| Flow | Use When | Mechanism | Refresh? | Notes |
|---|---|---|---|---|
| F&O S2S (Microsoft Entra) | F&O virtual entities in Dataverse | Entra app registration → S2S token | Per-request | CDSVirtualEntityApplication role required [src2] |
| OData v4 header auth | External OData endpoints | Custom header parameters | N/A | Up to 10 parameters [src4] |
| OData v4 query string | External OData endpoints | Custom query string params | N/A | Toggle between header/query string [src4] |
| Custom provider auth | Any external system | Developer-implemented | Developer-defined | Handle auth in .NET plug-in code [src8] |
START — User needs to access external data from Dataverse/Power Platform
├── Does the user need data replicated INTO Dataverse?
│ ├── YES — need auditing, search, calculated fields, or offline
│ │ ├── Real-time bidirectional? → Dual Write
│ │ └── Batch ETL? → DMF or Azure Synapse Link
│ └── NO — federated read/write acceptable
│ ├── External source is Finance & Operations?
│ │ ├── YES → F&O Virtual Entities (built-in provider)
│ │ └── NO ↓
│ ├── External source exposes OData v4?
│ │ ├── YES → OData v4 Data Provider (built-in)
│ │ └── NO ↓
│ ├── Azure Cosmos DB?
│ │ ├── YES → Cosmos DB provider (Marketplace)
│ │ └── NO → Custom Data Provider (.NET plug-ins)
├── Data volume
│ ├── < 1K records/day → Virtual entities fine
│ ├── 1K-10K records/day → Optimize with $select, pagination
│ └── > 10K records/day → Consider Dual Write or DMF instead
└── Need charts, dashboards, search, or auditing?
├── YES → Cannot use virtual entities; replicate to native tables
└── NO → Virtual entities are the right choice
| Step | Action | Where | Verification |
|---|---|---|---|
| 1 | Enable Power Platform integration | Power Platform admin center | Environment shows "Linked" |
| 2 | Install required solutions | Auto-installed with integration | Verify Dynamics365Company, MicrosoftOperationsVESupport, ERPCatalog, ERPVE |
| 3 | Find entity in catalog | Advanced Find → "Available finance and operations entities" | Entity appears in list |
| 4 | Make entity visible | Open entity → check "Visible" → Save | Virtual entity appears in Advanced Find |
| 5 | Refresh metadata (after F&O changes) | Open entity → check "Refresh" → Save | Updated columns appear |
| 6 | Reference in ISV solution | Add existing → Table → select virtual entity | Hard dependency on MicrosoftOperationsERPVE |
| OData EDM Type | Dataverse Type | Supported? | Notes |
|---|---|---|---|
| Edm.Guid | Unique Identifier | Yes | Required for primary key [src4] |
| Edm.String | Single/Multi Line Text | Yes | |
| Edm.Int32 | Whole Number | Yes | Cannot map to primary key [src4] |
| Edm.Int64 | Whole Number | Yes | |
| Edm.Decimal | Decimal/Currency | Yes | |
| Edm.Double | Floating Point | Yes | |
| Edm.Boolean | Two Options | Yes | |
| Edm.DateTime | Date and Time | Yes | |
| Edm.DateTimeOffset | Date and Time | Yes | |
| Edm.Binary | — | No | Not supported [src4] |
| Edm.Float | — | No | Not supported [src4] |
| Edm.Int16 | — | No | Not supported [src4] |
| Edm.Byte / Edm.SByte | — | No | Not supported [src4] |
| Edm.Time | — | No | Not supported [src4] |
Create a data source pointing to your external OData endpoint. [src4]
Verify: Navigate to Virtual Entity Data Sources list → new source appears with status Active.
Map your external entity to a Dataverse virtual table. [src1]
Verify: Open the new table in Advanced Find → records from external source appear.
Read records from the virtual table through the Dataverse Web API. [src6]
GET [Organization URI]/api/data/v9.2/new_externalproducts?$select=new_name,new_price,new_category&$filter=new_category eq 'Electronics'&$top=50
Accept: application/json
OData-MaxVersion: 4.0
OData-Version: 4.0
Verify: HTTP 200 response with value array containing external records.
Write operations work identically to native Dataverse tables. [src1, src7]
POST [Organization URI]/api/data/v9.2/new_externalproducts
Content-Type: application/json
{
"new_name": "Widget Pro",
"new_price": 29.99,
"new_category": "Electronics"
}
Verify: HTTP 204 response with OData-EntityId header. Confirm record exists in external source.
# Input: Microsoft Entra app credentials, Dataverse environment URL
# Output: List of F&O customer records surfaced as virtual entities
import requests
from msal import ConfidentialClientApplication
app = ConfidentialClientApplication(
client_id="YOUR_APP_ID",
client_credential="YOUR_CLIENT_SECRET",
authority="https://login.microsoftonline.com/YOUR_TENANT_ID"
)
token = app.acquire_token_for_client(
scopes=["https://YOUR_ORG.crm.dynamics.com/.default"]
)
headers = {
"Authorization": f"Bearer {token['access_token']}",
"OData-MaxVersion": "4.0",
"OData-Version": "4.0",
"Accept": "application/json",
"Prefer": "odata.maxpagesize=100"
}
# Use $select to avoid FK overhead
url = "https://YOUR_ORG.crm.dynamics.com/api/data/v9.2/mserp_custcustomerv3entities"
params = {
"$select": "mserp_customeraccount,mserp_organizationname,mserp_customergroupid",
"$filter": "mserp_customergroupid eq '10'",
"$top": "50"
}
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
customers = response.json().get("value", [])
// Input: IOrganizationService from Dataverse SDK
// Output: Created/updated/deleted record in external system
var record = new Entity("new_externalproduct");
record["new_name"] = "Widget Pro Max";
record["new_price"] = new Money(49.99m);
// Create passes through to external data source
Guid newId = service.Create(record);
// Update
record.Id = newId;
record["new_price"] = new Money(44.99m);
service.Update(record);
// Delete
service.Delete("new_externalproduct", newId);
# Input: Dataverse environment URL, F&O entity logical name
# Output: Virtual entity becomes visible in Dataverse
Install-Module -Name Microsoft.Xrm.Data.PowerShell -Scope CurrentUser
$conn = Connect-CrmOnline -ServerUrl "https://YOUR_ORG.crm.dynamics.com"
$catalog = Get-CrmRecords -conn $conn `
-EntityLogicalName "mserp_finabortenvironmententity" `
-FilterAttribute "mserp_physicalname" `
-FilterOperator "eq" `
-FilterValue "CustCustomerV3Entity"
$entityId = $catalog.CrmRecords[0].Id
Set-CrmRecord -conn $conn `
-EntityLogicalName "mserp_finabortenvironmententity" `
-Id $entityId `
-Fields @{ "mserp_hasbeengenerated" = $true }
| Source Type | Target Type | Transform | Gotcha |
|---|---|---|---|
| Edm.Guid | Unique Identifier | Direct | Must be primary key — no alternatives [src4] |
| Edm.Int32 | Whole Number | Direct | Cannot serve as primary key [src4] |
| Edm.String (>4000 chars) | Multiple Lines of Text | Direct | Check max length constraints on external side |
| Edm.DateTimeOffset | Date and Time | Timezone conversion | Dataverse stores UTC; external may use local time |
| Edm.Decimal | Currency | Currency entity mapping | Currency lookup must exist in Dataverse |
| Nullable=False properties | Business Required | Must align | Mismatch causes silent save failures [src4] |
$select in Retrieve/RetrieveMultiple — server-side projection is not applied. [src1]_mshr_fk_) trigger additional round-trips. Excluding them can reduce response time by 50-80%. [src6]| Code | Meaning | Cause | Resolution |
|---|---|---|---|
| 429 | Too Many Requests (Dataverse) | Exceeded 6,000 requests in 5-min window or 52 concurrent | Exponential backoff with Retry-After header [src5] |
| 429 (F&O) | Too Many Requests (resource-based) | High CPU/memory utilization on F&O web servers | Reduce concurrency; check throttling prioritization [src5] |
| 400 | Bad Request | Nullable property mismatch or unsupported EDM type | Verify external field nullability matches Dataverse field requirement [src4] |
| InternalServerError | F&O timeout | Query too complex or F&O service unavailable | Increase timeout; simplify query [src6] |
| "Unable to determine the user in CDS" | Missing user mapping | S2S call used SYSTEM user without proper context | Use org context with appropriate app/user with F&O permissions [src2] |
| Entity not found | Virtual entity not visible | Entity not enabled in catalog | Check "Visible" checkbox in catalog [src3] |
Always use $select to exclude _mshr_fk_ prefixed columns. [src6]Co-locate in same Azure region. [src2]Set Refresh checkbox on entity in catalog after schema changes. [src3]Assign CDSVirtualEntityAuthorizedPortalUser; remove sysadmin. [src2]Generate stable GUIDs (e.g., deterministic UUID v5) from external IDs in the data provider. [src1, src4]// BAD — fetches all columns including FK navigation properties
GET /api/data/v9.2/mshr_hcmworkerbaseentities
// Triggers cascading round-trips to F&O for each FK column
// Causes throttling on datasets > 100 records
// GOOD — explicitly select only needed columns
GET /api/data/v9.2/mshr_hcmworkerbaseentities?$select=mshr_name,mshr_firstname,mshr_lastname,mshr_personnelnumber
// 50-80% faster; avoids FK round-trips [src6]
// BAD — 50,000 sequential S2S calls via virtual entities
for (int i = 0; i < 50000; i++)
{
var record = new Entity("mserp_vendvendortablev2entity");
record["mserp_vendoraccountnumber"] = vendors[i].AccountNumber;
service.Create(record); // Each = round-trip to F&O
}
// Hours of runtime + throttled at 6,000/5min
// GOOD — use Data Management Framework for bulk operations
// Virtual entities are for real-time federated access, not ETL
// DMF supports parallel processing with configurable batch sizes
// Use DMF REST API: POST /data/DataManagementDefinitionGroups/
// Microsoft.Dynamics.DataEntities.ImportFromPackage
// BAD — relying on Dataverse audit for virtual entity records
// Auditing is NOT supported for virtual tables [src1]
// GOOD — use F&O entity change tracking or external audit logs
// For F&O: enable change tracking on data entity in AOT
// For custom providers: log changes in plug-in handlers
Always provision in same Azure region. [src2]Check Refresh checkbox after F&O schema changes; automate via CI/CD. [src3]Only enable actively needed entities; disable unused ones. [src3]Implement calculations on external side or OData computed view. [src1]Use Advanced Editor in Power Query; exclude _mshr_fk_ columns. [src6]CDSVirtualEntityAuthorizedPortalUser role only. [src2]# Check if F&O virtual entities solution is installed (PowerShell)
Get-CrmRecords -conn $conn -EntityLogicalName "solution" \
-FilterAttribute "uniquename" -FilterOperator "like" \
-FilterValue "MicrosoftOperations%"
# List all visible F&O virtual entities
GET [Org URI]/api/data/v9.2/mserp_finabortenvironmententities?\
$filter=mserp_hasbeengenerated eq true\
&$select=mserp_physicalname,mserp_hasbeengenerated
# Test virtual entity access (simple retrieve)
GET [Org URI]/api/data/v9.2/mserp_custcustomerv3entities?$top=1\
&$select=mserp_customeraccount
# Check API limits via response headers
# Look for: x-ms-ratelimit-burst-remaining-xrm-requests
# And: Retry-After header on 429 responses
# Enable plug-in tracing for virtual entity errors
# Settings > Administration > System Settings > Customization tab
# Then check: Settings > Plug-In Trace Log
| Feature | Release | Status | Notes |
|---|---|---|---|
| Virtual tables (OData v4 provider) | Dataverse 2017 | GA | Original release — read-only |
| CRUD support for OData v4 provider | 2020 Wave 2 | GA | Full Create, Update, Delete added [src1] |
| Custom data provider CRUD | 2020 Wave 2 | GA | Plug-in registration for all 5 operations [src8] |
| F&O virtual entities | F&O 10.0.12 | GA | Full CRUD for F&O OData entities [src2] |
| Azure Cosmos DB provider | 2021 | GA | Azure Marketplace [src1] |
| Virtual Table Creation Wizard | 2024 Wave 2 | Preview | No-code for SharePoint and SQL sources |
| User-based API limits removed for F&O | 10.0.36 | GA | Optional limits disabled; resource-based remain [src5] |
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Real-time federated access without data duplication | Need auditing, search, or calculated columns | Dual Write or DMF to replicate into native Dataverse tables |
| Power Apps/Power Automate needs F&O entity access | Bulk migration (>10K records) | Data Management Framework or Azure Synapse Link |
| Cross-app experiences (CE + F&O) without sync lag | Need charts, dashboards, or advanced visualizations | Replicate to Dataverse or Power BI DirectQuery |
| Low-volume CRUD (<1K operations/day) | Need field-level security or user-owned filtering | Native Dataverse tables with proper security model |
| Prototyping before committing to Dual Write | High-concurrency (>50 users on same virtual entity) | Dual Write for replicated low-latency access |
| Portal access to F&O with user-context security | Offline-capable mobile apps | Replicate to native tables for offline sync |
$select — this is a known platform behavior, not a bug. Performance optimization must happen at the external endpoint or via narrow client-side projections. [src1]