| name | dataverse |
| description | Expert guidance for Microsoft Dataverse development including Web API (OData), C# plugin development, custom APIs, security model, early-bound types, and FetchXML queries. Use when working with Dataverse tables, writing plugins, building custom APIs, configuring security roles, or querying data via the Web API. |
Dataverse Development
Expert guidance for Dataverse Web API, C# plugin development, custom APIs, security model, and data operations.
Triggers
Use this skill when you see:
- dataverse, common data service, cds
- dataverse web api, odata, fetchxml
- dataverse plugin, iplugin, iorganizationservice
- custom api, dataverse security, security role
- early bound, modelbuilder, dataverse sdk
Instructions
Web API (OData) Patterns
# Retrieve records with select and filter
GET [org]/api/data/v9.2/accounts?$select=name,revenue&$filter=revenue gt 1000000&$top=10
Authorization: Bearer {token}
Prefer: odata.include-annotations="*"
# Retrieve with expand (related records)
GET [org]/api/data/v9.2/accounts(00000000-0000-0000-0000-000000000001)?$select=name&$expand=contact_customer_accounts($select=fullname,emailaddress1;$top=5)
# Create a record
POST [org]/api/data/v9.2/accounts
Content-Type: application/json
{
"name": "Contoso Ltd",
"revenue": 5000000,
"primarycontactid@odata.bind": "/contacts(00000000-0000-0000-0000-000000000002)"
}
# Update a record
PATCH [org]/api/data/v9.2/accounts(00000000-0000-0000-0000-000000000001)
Content-Type: application/json
If-Match: *
{
"name": "Contoso Ltd (Updated)",
"revenue": 6000000
}
# Delete a record
DELETE [org]/api/data/v9.2/accounts(00000000-0000-0000-0000-000000000001)
# Batch request
POST [org]/api/data/v9.2/$batch
Content-Type: multipart/mixed;boundary=batch_boundary
--batch_boundary
Content-Type: application/http
Content-Transfer-Encoding: binary
GET accounts?$select=name&$top=5 HTTP/1.1
Accept: application/json
--batch_boundary--
FetchXML Queries
<fetch aggregate="true">
<entity name="opportunity">
<attribute name="estimatedvalue" alias="total_value" aggregate="sum" />
<attribute name="ownerid" alias="owner" groupby="true" />
<filter>
<condition attribute="statecode" operator="eq" value="0" />
<condition attribute="estimatedclosedate" operator="this-year" />
</filter>
</entity>
</fetch>
<fetch>
<entity name="account">
<attribute name="name" />
<attribute name="revenue" />
<link-entity name="contact" from="parentcustomerid" to="accountid" link-type="outer" alias="c">
<attribute name="fullname" />
<attribute name="emailaddress1" />
</link-entity>
<filter>
<condition attribute="statecode" operator="eq" value="0" />
</filter>
<order attribute="name" />
</entity>
</fetch>
Plugin Development (C#)
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using System;
namespace Contoso.Plugins
{
public class AccountPreCreate : IPlugin
{
public void Execute(IServiceProvider serviceProvider)
{
var context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
var serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
var service = serviceFactory.CreateOrganizationService(context.UserId);
var tracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
try
{
if (context.InputParameters.Contains("Target") &&
context.InputParameters["Target"] is Entity target)
{
tracingService.Trace("AccountPreCreate: Processing {0}", target.LogicalName);
if (!target.Contains("accountnumber"))
{
var query = new QueryExpression("account")
{
ColumnSet = new ColumnSet("accountnumber"),
TopCount = 1,
Orders = { new OrderExpression("createdon", OrderType.Descending) }
};
var results = service.RetrieveMultiple(query);
var nextNumber = results.Entities.Count > 0
? int.Parse(results.Entities[0].GetAttributeValue<string>("accountnumber") ?? "0") + 1
: 1001;
target["accountnumber"] = nextNumber.ToString();
tracingService.Trace("Set account number to {0}", nextNumber);
}
}
}
catch (Exception ex)
{
tracingService.Trace("AccountPreCreate Error: {0}", ex.ToString());
throw new InvalidPluginExecutionException(
"An error occurred in the Account Pre-Create plugin.", ex);
}
}
}
}
Plugin Registration
pac plugin init
dotnet build
Custom APIs
namespace Contoso.Plugins
{
public class CalculateDiscount : IPlugin
{
public void Execute(IServiceProvider serviceProvider)
{
var context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
var accountId = (string)context.InputParameters["AccountId"];
var orderTotal = (decimal)context.InputParameters["OrderTotal"];
decimal discountRate = orderTotal > 10000 ? 0.15m : 0.05m;
decimal discountedTotal = orderTotal * (1 - discountRate);
context.OutputParameters["DiscountRate"] = discountRate;
context.OutputParameters["DiscountedTotal"] = discountedTotal;
}
}
}
# Call custom API via Web API
POST [org]/api/data/v9.2/contoso_CalculateDiscount
Content-Type: application/json
{
"AccountId": "00000000-0000-0000-0000-000000000001",
"OrderTotal": 15000.00
}
Early-Bound Types
pac modelbuilder build
Security Model
Security Layers:
1. Security Roles - Define table-level CRUD privileges
2. Privilege Depth - User / Business Unit / Parent-Child BU / Organization
3. Column Security - Restrict access to specific columns
4. Row-Level Sharing - Share individual records with users/teams
5. Teams - Owner teams, Access teams, AAD group teams
6. Field Masking - Mask sensitive data in columns
Role Privilege Matrix:
┌───────────────┬────────┬──────┬────────┬────────┬────────┐
│ Entity │ Create │ Read │ Update │ Delete │ Append │
├───────────────┼────────┼──────┼────────┼────────┼────────┤
│ Account │ BU │ Org │ BU │ User │ BU │
│ Contact │ BU │ Org │ BU │ User │ BU │
│ Opportunity │ User │ BU │ User │ None │ User │
└───────────────┴────────┴──────┴────────┴────────┴────────┘
Depth: User < BU < Parent-Child BU < Organization
Best Practices
| Practice | Description |
|---|
| Web API pagination | Use @odata.nextLink for large result sets; default page size is 5000 |
| Select columns | Always use $select to retrieve only needed columns |
| Plugin idempotency | Design plugins to produce the same result if executed multiple times |
| Tracing | Use ITracingService extensively for debugging in plugin trace logs |
| Pre vs Post | Use pre-operation for validation/modification, post-operation for side effects |
| Async plugins | Register long-running logic as async to avoid blocking the user |
| Early-bound | Use pac modelbuilder build for type-safe entity access in plugins |
| Least privilege | Grant minimum required security role privileges |
Common Workflows
New Plugin Development
pac plugin init to scaffold project
- Implement
IPlugin.Execute with business logic
- Build and test locally
- Register assembly with Plugin Registration Tool
- Register step (message, entity, stage)
- Test in Dataverse environment
- Add to solution for ALM
Web API Integration
- Register Azure AD app with Dataverse permissions
- Obtain OAuth 2.0 token (client credentials or auth code)
- Use
$select, $filter, $expand for efficient queries
- Handle pagination with
@odata.nextLink
- Use batch operations for bulk operations
- Implement retry logic for throttling (429 responses)