Dataverse Web API Laboratory: OAuth, OData, HTTP Contracts, and Resilient C# Integration
Engineering direct REST integrations with Microsoft Dataverse through Microsoft Entra ID, OAuth 2.0, OData v4, conditional requests, pagination, and service protection limits.
Power Platform Engineering Academy — Five Years in Five Days
Technical domain: Microsoft Dataverse
Application: APP-006 — Dataverse Web API Laboratory
Technologies: C#, .NET, HttpClient, Microsoft Entra ID, MSAL, OAuth 2.0, OData, Dataverse Web API, Model-Driven Apps
Status: Pre-implementation engineering design
Introduction
The Dataverse SDK provides a convenient programming abstraction over the Microsoft Dataverse service.
However, understanding the underlying HTTP interface is essential for engineers designing enterprise integrations.
External applications, Azure Functions, middleware services, custom connectors, and non-.NET systems frequently interact with Dataverse through its Web API.
The Dataverse Web API is an OData v4-based REST interface. It exposes tables, relationships, metadata, actions, functions, and other platform operations through HTTP.
Using the Web API directly requires a deeper understanding of authentication, authorization, URI construction, HTTP methods, JSON serialization, concurrency, pagination, error handling, and service protection.
A successful HTTP request is not necessarily evidence of a correct integration.
An HTTP 204 No Content response may represent a successful update without returning a record body. A 429 Too Many Requests response may indicate service protection throttling. A network timeout may occur after the server has already committed a request.
These distinctions become critical in production systems.
APP-006 — Dataverse Web API Laboratory investigates these behaviors through an independent C# REST client, a dedicated Dataverse data model, and a functional Model-Driven App.
Our central engineering question is:
How do we build a secure, correct, observable, and resilient Dataverse integration when working directly with HTTP rather than relying on the Dataverse SDK?
1. Understanding the Dataverse Web API Contract
A Dataverse Web API endpoint follows this general structure:
https://<organization>.crm.dynamics.com/api/data/v9.2/
The actual environment URL must be obtained from the target Dataverse environment.
The API exposes resources through entity sets.
For example, a custom table with the logical name:
pea_apirequest
might have an entity set name such as:
pea_apirequests
However, developers must not construct entity set names by blindly pluralizing table logical names.
The entity set name is metadata-defined and should be discovered from the environment.
1.1 Standard HTTP methods
| HTTP method | Typical Dataverse operation |
|---|---|
GET | Retrieve records or metadata |
POST | Create records or invoke supported operations |
PATCH | Update or Upsert records |
DELETE | Delete records or remove supported associations |
The meaning of a request depends on its URL, headers, and body.
For example:
GET /api/data/v9.2/accounts
retrieves records from the accounts entity set, subject to the caller’s privileges.
POST /api/data/v9.2/accountsContent-Type: application/json{ "name": "Contoso"}
requests creation of an account.
The Web API is not simply a thin HTTP wrapper around arbitrary database access. Dataverse evaluates operations through its platform security and business logic.
Reference: Use the Microsoft Dataverse Web API.
2. Authentication and Authorization
The Dataverse Web API uses Microsoft Entra ID for authentication.
An external application must obtain an access token appropriate for the Dataverse resource.
The token is sent using the HTTP Authorization header:
Authorization: Bearer <access_token>
Authentication proves the identity under which the request is made.
Authorization determines which Dataverse operations that identity may perform.
These are separate concerns.
2.1 Delegated authentication
In delegated authentication, the application acts on behalf of a signed-in user.
The effective access depends on the authenticated user and the relevant application authorization configuration.
This model is appropriate for interactive applications where user identity is meaningful.
2.2 Application authentication
In application authentication, a confidential client obtains a token using its own credentials.
For Dataverse, the application must also be configured with an appropriate application user and security roles in the target environment.
Obtaining a valid Microsoft Entra ID token does not automatically grant unrestricted Dataverse privileges.
The application user must have the permissions required to access the relevant tables and perform the requested operations.
2.3 Authentication model for APP-006
The laboratory will use the OAuth 2.0 client credentials flow with MSAL.
This allows the C# console application to authenticate without an interactive user.
The required configuration will be supplied through environment variables:
DATAVERSE_URLENTRA_TENANT_IDENTRA_CLIENT_IDENTRA_CLIENT_SECRET
Client secrets must not be committed to source control or embedded in application code.
For production workloads, certificate-based authentication or managed identity should be evaluated according to the hosting environment and supported authentication flow.
Reference: Authenticate to Microsoft Dataverse with the Web API.
3. Application Architecture
APP-006 will use the following architecture:
APP-006 Dataverse Web API Laboratory│├── C# REST Client│ ├── MSAL Authentication│ ├── HttpClient│ ├── GET / POST / PATCH / DELETE│ ├── OData Queries│ ├── Pagination│ ├── Error Handling│ ├── Retry Policy│ └── HTTP Diagnostics│├── Microsoft Dataverse│ ├── pea_apirequest│ ├── pea_apioperation│ └── pea_apiexecution│└── Model-Driven App ├── Entry Dashboard ├── API Requests ├── API Operations ├── Execution Diagnostics └── Failure Analysis
The C# client performs direct HTTP operations.
Dataverse persists the business records and diagnostic data.
The Model-Driven App exposes the results and provides an operational interface for inspecting the experiment.
4. Dataverse Data Model
The laboratory introduces three custom tables.
4.1 API Request
Logical name: pea_apirequest
| Column | Data type | Purpose |
|---|---|---|
pea_name | Primary name | Request identifier |
pea_externalid | Single line of text | External request key |
pea_description | Multiple lines of text | Request description |
pea_amount | Decimal number | Example numeric payload |
pea_status | Choice | Request lifecycle |
pea_requestedon | Date and time | Request timestamp |
An alternate key on pea_externalid may be introduced to reuse the lessons from APP-004.
4.2 API Operation
Logical name: pea_apioperation
| Column | Data type | Purpose |
|---|---|---|
pea_name | Primary name | Operation identifier |
pea_method | Single line of text | HTTP method |
pea_resource | Single line of text | Target resource |
pea_expectedstatus | Whole number | Expected HTTP status |
pea_description | Multiple lines of text | Operation objective |
4.3 API Execution
Logical name: pea_apiexecution
| Column | Data type | Purpose |
|---|---|---|
pea_name | Primary name | Execution identifier |
pea_method | Single line of text | HTTP method |
pea_resource | Single line of text | Target resource |
pea_statuscode | Whole number | HTTP response status |
pea_success | Yes/No | Expected outcome matched |
pea_durationms | Whole number | Client-observed duration |
pea_correlationid | Single line of text | Diagnostic correlation |
pea_responsebody | Multiple lines of text | Sanitized response |
pea_executedon | Date and time | Execution timestamp |
Response bodies must be sanitized before persistence.
Authentication tokens, secrets, and sensitive business data should not be written to diagnostic tables.
5. HTTP Headers and Content Negotiation
Dataverse Web API requests use standard HTTP headers together with OData-specific conventions.
A typical request includes:
Accept: application/jsonOData-Version: 4.0OData-MaxVersion: 4.0Authorization: Bearer <access_token>
Requests with JSON bodies also use:
Content-Type: application/json
Additional headers affect behavior.
| Header | Purpose |
|---|---|
Authorization | Supplies OAuth access token |
Accept | Specifies acceptable response representation |
Content-Type | Describes request body format |
Prefer: return=representation | Requests a representation after supported write operations |
If-Match | Supports conditional updates |
If-None-Match | Supports conditional create and cache semantics |
Prefer: odata.maxpagesize=... | Requests a page-size limit |
Retry-After | Communicates server-requested retry delay |
A robust client must understand the meaning of these headers rather than simply copying them into every request.
6. CRUD Operations Through HTTP
6.1 Create
POST /api/data/v9.2/pea_apirequestsContent-Type: application/json{ "pea_name": "API Test 001", "pea_externalid": "EXT-001", "pea_amount": 250.00}
A successful create commonly returns 204 No Content with an OData-EntityId response header identifying the created record.
Using Prefer: return=representation can request a returned representation, resulting in a different response shape and status.
6.2 Retrieve
GET /api/data/v9.2/pea_apirequests(<record-guid>)?$select=pea_name,pea_amountAccept: application/json
The $select parameter restricts the returned columns.
6.3 Update
PATCH /api/data/v9.2/pea_apirequests(<record-guid>)Content-Type: application/jsonIf-Match: *{ "pea_amount": 300.00}
If-Match: * prevents the operation from creating a missing record.
6.4 Delete
DELETE /api/data/v9.2/pea_apirequests(<record-guid>)If-Match: *
Deletion must be treated as a state-changing operation.
The client should not assume that repeating a deletion produces the same HTTP response, even when the intended final state remains that the record is absent.
Reference: Create a table row using the Web API.
7. OData Query Engineering
The Web API supports OData query options.
For example:
GET /api/data/v9.2/pea_apirequests?$select=pea_name,pea_amount&$filter=pea_amount gt 100&$orderby=pea_amount desc
The query requests:
- Two columns.
- Records with an amount greater than 100.
- Descending order by amount.
The query must be properly URI-encoded by the HTTP client.
7.1 Filtering
Common OData operators include:
eqnegtgeltleandor
Dataverse also supports documented query functions and platform-specific capabilities.
7.2 Expanding relationships
The $expand option can retrieve related information using navigation properties.
The navigation property name must be obtained from metadata.
It should not be guessed from the lookup column name.
This connects directly to the relationship metadata concepts from APP-003.
7.3 Pagination
When a query returns more records than fit in one page, Dataverse may include:
{ "@odata.nextLink": "https://example.crm.dynamics.com/api/data/v9.2/..."}
The client must follow the returned next-link URL.
It should not construct its own paging token or modify the token returned by Dataverse.
Reference: Page results using OData.
8. Error Handling and Service Protection
Dataverse enforces service protection limits to protect platform availability.
Clients can receive HTTP 429 Too Many Requests responses when limits are exceeded.
The response may include Retry-After, indicating how long the client should wait before retrying.
A resilient integration should:
- Recognize throttling responses.
- Respect the server-provided retry delay.
- Avoid aggressive retry loops.
- Limit concurrent requests.
- Record diagnostic information.
- Distinguish transient failures from permanent errors.
However, retries must be designed around operation semantics.
Automatically retrying a read is different from retrying a write whose outcome is unknown.
A timed-out POST may have created a record.
Repeating it without a stable identity or deduplication mechanism can create duplicates.
This connects directly to APP-004’s alternate-key and Upsert experiments.
Reference: Service protection API limits.
9. Independent C# REST Client
The proposed project is:
PowerPlatformEngineeringAcademy/└── Dataverse/ └── APP006.DataverseWebApiLab/ ├── APP006.DataverseWebApiLab.csproj └── Program.cs
Create the project:
dotnet new console --name APP006.DataverseWebApiLabcd APP006.DataverseWebApiLabdotnet add package Microsoft.Identity.Client
The application uses HttpClient directly rather than ServiceClient.
This is intentional.
The objective is to observe the HTTP contract, authentication flow, response status, headers, and JSON payloads without hiding them behind SDK abstractions.
The following code assumes that the Dataverse application user, security roles, tables, and columns have already been configured.
10. Complete C# Web API Client
The following Program.cs implements the initial HTTP laboratory.
It authenticates using MSAL, creates a record, retrieves it, updates it, queries records, and deletes the test record.
It also captures HTTP status codes, response times, and selected diagnostic headers.
For clarity, the initial implementation does not automatically retry write operations. This prevents the experiment from accidentally hiding ambiguous write outcomes.
Program.cs
<CodeBlock language=”csharp” editable>
using System;
using System.Diagnostics;
using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Identity.Client;
string Required(string name) =>
Environment.GetEnvironmentVariable(name)
?? throw new InvalidOperationException(
$”Missing environment variable: {name}”);
string dataverseUrl = Required(“DATAVERSE_URL”)
.TrimEnd(‘/’);
string tenantId = Required(“ENTRA_TENANT_ID”);
string clientId = Required(“ENTRA_CLIENT_ID”);
string clientSecret = Required(“ENTRA_CLIENT_SECRET”);
const string EntitySet = “pea_apirequests”;
var app = ConfidentialClientApplicationBuilder
.Create(clientId)
.WithClientSecret(clientSecret)
.WithAuthority(
$”https://login.microsoftonline.com/{tenantId}“)
.Build();
var authentication = await app
.AcquireTokenForClient(
new[] { $”{dataverseUrl}/.default” })
.ExecuteAsync();
using var http = new HttpClient
{
BaseAddress = new Uri(
$”{dataverseUrl}/api/data/v9.2/”),
Timeout = TimeSpan.FromSeconds(60)
};
http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue(
“Bearer”,
authentication.AccessToken);
http.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue(
“application/json”));
http.DefaultRequestHeaders.Add(
“OData-Version”, “4.0”);
http.DefaultRequestHeaders.Add(
“OData-MaxVersion”, “4.0”);
async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
string operation)
{
var stopwatch = Stopwatch.StartNew();
try{ HttpResponseMessage response = await http.SendAsync(request); stopwatch.Stop(); Console.WriteLine( $"{operation}: HTTP {(int)response.StatusCode}, " + $"{stopwatch.ElapsedMilliseconds} ms"); if (response.Headers.TryGetValues( "x-ms-service-request-id", out var correlationIds)) { Console.WriteLine( $"Correlation: {string.Join(",", correlationIds)}"); } if (response.StatusCode == HttpStatusCode.TooManyRequests) { Console.WriteLine( $"Retry-After: {response.Headers.RetryAfter}"); } if (!response.IsSuccessStatusCode) { string error = await response.Content .ReadAsStringAsync(); Console.Error.WriteLine( $"Request failed: {error}"); } return response;}catch (Exception ex){ stopwatch.Stop(); Console.Error.WriteLine( $"{operation} failed after " + $"{stopwatch.ElapsedMilliseconds} ms: " + ex.Message); throw;}
}
string externalId = $”APP006-{Guid.NewGuid()}”;
// CREATE
using var createRequest =
new HttpRequestMessage(
HttpMethod.Post,
EntitySet);
createRequest.Headers.TryAddWithoutValidation(
“Prefer”,
“return=representation”);
createRequest.Content = JsonContent.Create(new
{
pea_name = “Web API Laboratory”,
pea_externalid = externalId,
pea_amount = 250.00m
});
using var createResponse = await SendAsync(
createRequest,
“CREATE”);
createResponse.EnsureSuccessStatusCode();
using JsonDocument created = JsonDocument.Parse(
await createResponse.Content.ReadAsStringAsync());
Guid recordId = created.RootElement
.GetProperty(“pea_apirequestid”)
.GetGuid();
Console.WriteLine($”Created record: {recordId}”);
// RETRIEVE
using var getRequest = new HttpRequestMessage(
HttpMethod.Get,
$”{EntitySet}({recordId})” +
“?$select=pea_name,pea_externalid,pea_amount”);
using var getResponse = await SendAsync(
getRequest,
“RETRIEVE”);
getResponse.EnsureSuccessStatusCode();
string retrievedJson =
await getResponse.Content.ReadAsStringAsync();
Console.WriteLine(retrievedJson);
// UPDATE ONLY
using var updateRequest = new HttpRequestMessage(
HttpMethod.Patch,
$”{EntitySet}({recordId})”);
updateRequest.Headers.TryAddWithoutValidation(
“If-Match”, “*”);
updateRequest.Content = JsonContent.Create(new
{
pea_amount = 300.00m
});
using var updateResponse = await SendAsync(
updateRequest,
“UPDATE”);
updateResponse.EnsureSuccessStatusCode();
// QUERY
string queryUrl =
$”{EntitySet}” +
“?$select=pea_name,pea_externalid,pea_amount” +
$”&$filter=pea_externalid eq ‘{externalId}’”;
using var queryRequest = new HttpRequestMessage(
HttpMethod.Get,
queryUrl);
using var queryResponse = await SendAsync(
queryRequest,
“QUERY”);
queryResponse.EnsureSuccessStatusCode();
string queryJson =
await queryResponse.Content.ReadAsStringAsync();
Console.WriteLine(queryJson);
// DELETE
using var deleteRequest = new HttpRequestMessage(
HttpMethod.Delete,
$”{EntitySet}({recordId})”);
deleteRequest.Headers.TryAddWithoutValidation(
“If-Match”, “*”);
using var deleteResponse = await SendAsync(
deleteRequest,
“DELETE”);
deleteResponse.EnsureSuccessStatusCode();
Console.WriteLine(“Web API laboratory completed.”);
</CodeBlock>
10.1 Important implementation qualifications
This sample assumes that the primary key is named pea_apirequestid, which must be verified against the published table metadata.
It also assumes that the environment’s entity set name is pea_apirequests.
The application must not use these names without confirming the actual Dataverse schema.
The sample captures diagnostic information in the console but does not yet persist execution records in pea_apiexecution.
It also does not implement complete pagination or automated retry policies.
These are deliberate extensions for the practical experiment.
11. Conditional Requests and Optimistic Concurrency
Dataverse supports ETags for conditional operations.
A retrieved record may include an annotation such as:
{ "@odata.etag": "W/\"1234567\"", "pea_name": "Web API Laboratory"}
The ETag can be used in a conditional update.
PATCH /api/data/v9.2/pea_apirequests(<record-guid>)If-Match: W/"1234567"Content-Type: application/json{ "pea_amount": 350.00}
If the record has changed and the supplied ETag no longer matches, Dataverse can reject the request with HTTP 412 Precondition Failed.
This provides a mechanism for optimistic concurrency.
The difference between If-Match: * and an exact ETag is important.
If-Match: * prevents creation of a missing record.
An exact ETag also verifies the record version.
This topic will be explored in depth in APP-009.
Reference: Perform conditional operations using the Web API.
12. Resilience and Retry Engineering
The initial client should be extended with a retry policy for appropriate transient failures.
A practical classification is:
| HTTP outcome | General handling |
|---|---|
200 OK | Process returned data |
201 Created | Process created representation |
204 No Content | Treat as successful operation without a body |
400 Bad Request | Inspect request contract |
401 Unauthorized | Investigate authentication |
403 Forbidden | Investigate authorization |
404 Not Found | Investigate resource identity and permissions |
412 Precondition Failed | Handle concurrency conflict |
429 Too Many Requests | Respect Retry-After |
5xx | Evaluate transient failure and retry safety |
A retry strategy must distinguish read operations from writes.
For GET requests, retries are generally less risky because they do not intentionally modify business state.
For POST requests, retries require more care.
A timeout does not establish whether the server committed the request.
For repeatable creation, use a stable external identifier and an appropriate create-only or Upsert contract.
For non-idempotent operations, introduce deduplication mechanisms where necessary.
13. Security Architecture
The laboratory must distinguish three identities:
Application registration: The Microsoft Entra ID application representing the client.
Application user: The Dataverse identity associated with that application in the environment.
Security role: The privileges assigned to that Dataverse identity.
The application registration alone is insufficient.
The Dataverse application user must have the required table privileges.
For this experiment, the application user needs access to the laboratory tables and permission to create, read, update, and delete the relevant records.
In production, permissions should be limited to the minimum required operations.
Client secrets should be stored securely, and the application should avoid logging tokens or sensitive authentication information.
14. Model-Driven App: PEA Dataverse Web API Laboratory
The operational application will be named:
PEA Dataverse Web API Laboratory
Entry dashboard
The dashboard will expose:
| Metric | Purpose |
|---|---|
| Total API executions | Number of recorded operations |
| Successful requests | Operations matching expected outcomes |
| Failed requests | Operations with unexpected outcomes |
| Average duration | Mean client-observed response time |
| HTTP 429 count | Recorded throttling responses |
| Latest execution | Most recent operation |
The dashboard should also contain a recent-executions view and a status-code distribution chart.
API Request form
The form will display the test record’s external identifier, amount, description, and status.
API Operation form
The form will document the intended HTTP method, resource, and expected response.
API Execution form
The form will expose HTTP status, response duration, correlation information, sanitized response content, and execution timestamp.
These forms provide the operational interface for inspecting the results of the REST client.
15. Failure Injection Laboratory
The experiment should deliberately trigger controlled failures.
| Experiment | Expected behavior |
|---|---|
| Missing access token | Authentication failure |
| Invalid token audience | Request rejected |
| Application user without required privilege | Authorization failure |
| Invalid entity set | Resource error |
| Invalid column name | Request validation error |
| Invalid JSON | Request rejected |
Update missing record with If-Match: * | No unintended record creation |
| Update using stale ETag | Concurrency failure |
Follow @odata.nextLink | Retrieve subsequent pages |
| Exceed service protection limits | HTTP 429 and retry guidance |
| Retry ambiguous POST | Evaluate duplicate-creation risk |
Service protection testing should be controlled and should not deliberately overwhelm a shared production environment.
The objective is to observe the platform’s response and validate client behavior, not to maximize request volume without limits.
16. Engineering Comparison: SDK Versus Web API
| Dimension | Dataverse SDK | Direct Web API |
|---|---|---|
| Programming model | .NET object model | HTTP and JSON |
| Authentication | Supported client abstractions | Explicit OAuth handling |
| Query construction | QueryExpression, FetchXML, LINQ | OData, FetchXML |
| HTTP visibility | Abstracted by client | Directly observable |
| Retry behavior | Depends on SDK client | Implemented or configured by client |
| Cross-language support | Primarily .NET | Any HTTP-capable platform |
| Metadata operations | SDK requests | Metadata endpoints |
| Concurrency | SDK concurrency mechanisms | ETags and conditional headers |
Neither interface is universally superior.
The SDK is often appropriate for .NET applications that benefit from Dataverse-specific abstractions.
The Web API is especially useful for cross-platform integrations, custom connectors, and systems where HTTP-level control is important.
Architectural decisions should be based on the integration requirements rather than a blanket preference for one interface.
17. Microsoft Learn Technical References
- Use the Microsoft Dataverse Web API — Web API architecture and capabilities.
- Authenticate to Microsoft Dataverse with the Web API — Authentication approaches.
- Use OAuth with Microsoft Dataverse — OAuth authentication architecture.
- Create a table row using the Web API — Create operations and response behavior.
- Query data using the Web API — OData queries.
- Page results using OData — Pagination.
- Perform conditional operations using the Web API — ETags and conditional requests.
- Service protection API limits — Throttling and retry behavior.
Conclusion
The Dataverse Web API provides direct access to platform operations through standard HTTP and OData contracts.
This flexibility comes with engineering responsibilities.
Developers must manage authentication, understand the effective security identity, construct correct requests, interpret status codes, handle pagination, and design retries around the actual semantics of each operation.
The most important architectural lesson is:
A reliable REST integration is not defined by successful HTTP requests alone. It is defined by correct identity, explicit contracts, safe failure handling, and observable business outcomes.
APP-006 establishes the foundation for more advanced experiments involving Dataverse SDK internals, transactions, concurrency, Custom APIs, Azure Functions, and enterprise integration architecture.
The implementation remains pending execution and inspection in a real Dataverse environment.
Power Platform Engineering Academy — Five Years in Five Days
Article 006/100 — Dataverse Web API Laboratory
