Dataverse Deep Dive: Virtual Tables, Plug-Ins, and Entra ID Role Design

by G.R Badhon

Architecture at a glance

Dataverse security is anchored in Entra ID. Users are added to Entra ID security groups that back Dataverse group teams. Those teams carry the security roles; when a user signs in, Conditional Access evaluates the session, membership maps to one or more group teams, and the teams’ roles determine exactly what the user can do-no direct per‑user role assignments are needed. Records that should be broadly shared are team‑owned; sensitive or personal records remain user‑owned and are shared explicitly when required.

From a data perspective, the core domain is straightforward: Account places an Order; each Order contains many Order Lines; every Order Line references a single Product. Live stock and pricing signals come from ExternalInventory, a virtual table mastered in the line‑of‑business system. Application logic (typically a synchronous plug‑in) queries ExternalInventory during save to validate requested quantities and to surface immediate errors to the user, while the authoritative writes remain inside Dataverse tables.

In sequence: a user signs in → Conditional Access policy passes → Entra ID group membership resolves → corresponding Dataverse group teams are applied → team‑assigned roles grant privileges on Account, Order, Order Line, and Product → plug‑ins enforce business rules and consult ExternalInventory in real time → the transaction commits or is rejected with a friendly message.

Relationship summary in words: Account 1..* Order; Order 1..* Order Line; *Order Line ..1 Product. ExternalInventory is read‑through only and related to Product by an alternate key or shared identifier so it can be queried efficiently during validation.

Virtual tables in practice

Patterns that work well

  1. Read heavy reference data that must remain the source of truth in the external system. Example: inventory, pricing, catalog, compliance lists.
  2. Large datasets that would be expensive to replicate into Dataverse but are needed for search and lookup.
  3. Near real time visibility without an ETL schedule. Consumers always see the latest state in the external store.

Where to avoid

  1. High write workloads or complex multi record transactions. Dataverse cannot provide a single transaction that spans external stores.
  2. Features that rely on Dataverse storage engine capabilities like full text relevance search over the external rows, duplicate detection, offline, or heavy aggregation.

Performance notes

  • Filter early and narrow the column set. Use QueryExpression with ColumnSet on just the fields you will use.
  • Prefer server side paging. Avoid client side loops that page one record at a time.
  • Join on keys that exist in the external source. If you can, create an alternate key on the virtual table and point relationships to that key.
  • Expect provider throttling. Add retry with backoff in custom providers and in consuming code.
  • Measure latency for every query path because round trips go all the way to the external data source.

Provider choices

  • Built in providers such as OData v4 and SQL can get you started quickly. For complex security or shape changes, implement a custom data provider so you control filtering and projection.

Plug-ins vs Power Automate

When you need low latency, strong validation, or atomicity with the database write, use a synchronous C# plug-in. When you need integration with other cloud services, human in the loop approvals, or long running orchestration, use Power Automate.

| Scenario                         | C# Plug-in                         | Power Automate                      |
|----------------------------------|------------------------------------|-------------------------------------|
| Pre save validation              | Best fit in PreOperation           | Possible but slower                 |
| Post commit side effects         | Use PostOperation with care        | Good fit with connectors            |
| Cross record atomic updates      | Use IOrganizationService in txn    | Not atomic                          |
| External API call with retries   | HttpClient inside plugin or Azure  | Excellent with dedicated connectors |
| Citizen developer ownership      | Less friendly                       | More friendly                        | 

Rule of thumb: if failure must block the write and users must see errors immediately in the form, choose a synchronous plug-in. If eventual consistency is acceptable, choose a flow.

Sample code

1. C# plug in skeleton with virtual table lookup

using System;
using System.Net.Http;
using System.ServiceModel; // FaultException
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;

public class OrderCreatePreValidate : IPlugin
{
    public void Execute(IServiceProvider serviceProvider)
    {
        var context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
        var factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
        var service = factory.CreateOrganizationService(context.UserId);

        if (context.PrimaryEntityName != "salesorder" || context.MessageName != "Create") return;

        var target = (Entity)context.InputParameters["Target"];
        if (!target.Contains("new_productid") || !target.Contains("new_quantity")) return;

        var product = target.GetAttributeValue<EntityReference>("new_productid");
        var qty = target.GetAttributeValue<decimal>("new_quantity");

        // Virtual table logical name: new_externalinventory
        var qe = new QueryExpression("new_externalinventory")
        {
            ColumnSet = new ColumnSet("new_product", "new_availableqty"),
            Criteria = new FilterExpression(LogicalOperator.And)
        };
        qe.Criteria.AddCondition("new_product", ConditionOperator.Equal, product.Id);

        var results = service.RetrieveMultiple(qe);
        if (results.Entities.Count == 0)
            throw new InvalidPluginExecutionException("Inventory not found in external system.");

        var available = results.Entities[0].GetAttributeValue<decimal>("new_availableqty");
        if (qty > available)
            throw new InvalidPluginExecutionException("Requested quantity exceeds available inventory.");

        // Optional: set a flag that will be used in PostOperation
        target["new_inventoryvalidated"] = true;
    }
} 

2. Common Dataverse operations with IOrganizationService

// Create
var account = new Entity("account");
account["name"] = "Contoso";
var accountId = service.Create(account);

// Update
var update = new Entity("account") { Id = accountId };
update["telephone1"] = "+44 20 7946 0000";
service.Update(update);

// Associate account to a group team as owner
var teamRef = new EntityReference("team", teamId);
var accountRef = new EntityReference("account", accountId);
service.Associate("team", teamId, new Relationship("team_account"),
    new EntityReferenceCollection { accountRef });

// RetrieveMultiple with paging
var page = 1; var more = true; string cookie = null;
while (more)
{
    var qe = new QueryExpression("contact") { ColumnSet = new ColumnSet("fullname"), PageInfo = new PagingInfo { PageNumber = page, Count = 500, PagingCookie = cookie } };
    var resp = service.RetrieveMultiple(qe);
    foreach (var e in resp.Entities) { /* process */ }
    more = resp.MoreRecords; cookie = resp.PagingCookie; page++;
} 

Role design with Entra ID groups and Conditional Access

Start with group based access. Create Entra ID security groups that map to business functions. Link each group to a Dataverse group team. Assign Dataverse security roles to the team, not to individuals. This model means joiners and leavers are handled by Entra ID only.

Use layered roles. System Administrator and Environment Admin should be eligible instead of permanent. Use Entra Privileged Identity Management to grant those roles just in time. For makers, use roles like Basic User, Environment Maker, and table specific custom roles that include only the privileges required.

Apply Conditional Access to Power Apps and Dynamics service principals. Typical policies include multi factor authentication, device compliance required for interactive sign in, named locations for corporate offices, and session risk based access control. If a subset of users needs to bypass Conditional Access for automation, use an application user with a certificate credential and scope its permissions to a minimal custom role. Do not assign broad roles like System Administrator to application users.

Control environment access. Set the environment security group so that only members can sign in. Place sandbox and production in separate Entra ID groups, since membership often differs.

Group team flow

[Entra ID Group A]
        |
        v
[Dataverse Group Team A] --assigned--> [Role: Sales Standard]
        |
        +-- owns records created by the team 

Tip: when a record should be shared across a population, prefer team ownership. Team-owned records are easier to share with another team through Access Teams or explicit share.

ALM and solutions

Package everything inside solutions so that deployment is repeatable. Unmanaged in development, managed in test and production. Use environment variables for external endpoints and tenant specific identifiers. Use connection references for flows and for virtual connector data providers so that deployments to new environments do not require edits.

Automate with the Power Platform CLI and a pipeline runner of your choice. Version solutions on each release. Keep plug in assemblies strong named and avoid breaking changes in public classes between versions.

# Auth once per agent
pac auth create --url https://org.crm.dynamics.com --applicationId <appId> --tenant <tenantId> --clientSecret <secret>

# Export and unpack for source control
pac solution export --name Contoso.Core --path out --managed false
pac solution unpack --zipFile out/Contoso.Core.zip --folder src/solution --processCanvasApps true

# Pack and import as managed for prod
pac solution pack --folder src/solution --zipFile out/Contoso.Core_managed.zip
pac solution import --path out/Contoso.Core_managed.zip --activate-plugins true 

Environment variables to define

  • Api.Inventory.BaseUrl
  • Api.Inventory.TimeoutSeconds
  • FeatureFlags.InventoryValidation

Connection references to define

  • Connection for any Power Automate flows
  • Connection for a virtual connector based data provider

Operational notes

  • Telemetry: capture plug in execution time, failures, and rejections in Application Insights. Include context like correlation id and user id.
  • Backfill: if you need a physical copy for reporting, run a scheduled export to a staging table and keep the virtual table for fresh lookups.
  • Security review: export the role to privilege matrix from the solution and stash it in the repo for audits.

Security model template

Use this starter and adapt it to your tables and groups.

# security-model.yml
roles:
  - name: Sales Standard
    privileges:
      account: Read:Org, Create:BusinessUnit, Write:BusinessUnit, Append:Org, AppendTo:Org
      contact: Read:Org, Create:BusinessUnit, Write:BusinessUnit, Append:Org, AppendTo:Org
      salesorder: Read:BusinessUnit, Create:BusinessUnit, Write:BusinessUnit, Append:Org, AppendTo:Org
    environment:
      customization: None
  - name: Maker Minimal
    privileges:
      solution: Read:Org, Create:Org, Write:Org
      environmentvariabledefinition: Read:Org, Create:Org, Write:Org
      pluginassembly: None
teams:
  - name: Sales Users Team
    type: group
    entraGroupObjectId: 00000000-0000-0000-0000-000000000000
    assignedRoles:
      - Sales Standard
conditionalAccess:
  - policy: Require MFA and compliant device for interactive sign in
    targetApps: ["Power Apps", "Dynamics 365"]
    users: ["Sales Users Team"]
    grantControls: ["Require multi factor authentication", "Require device to be marked as compliant"] 

You may also like