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 methodTypical Dataverse operation
GETRetrieve records or metadata
POSTCreate records or invoke supported operations
PATCHUpdate or Upsert records
DELETEDelete 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/accounts
Content-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_URL
ENTRA_TENANT_ID
ENTRA_CLIENT_ID
ENTRA_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

ColumnData typePurpose
pea_namePrimary nameRequest identifier
pea_externalidSingle line of textExternal request key
pea_descriptionMultiple lines of textRequest description
pea_amountDecimal numberExample numeric payload
pea_statusChoiceRequest lifecycle
pea_requestedonDate and timeRequest 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

ColumnData typePurpose
pea_namePrimary nameOperation identifier
pea_methodSingle line of textHTTP method
pea_resourceSingle line of textTarget resource
pea_expectedstatusWhole numberExpected HTTP status
pea_descriptionMultiple lines of textOperation objective

4.3 API Execution

Logical name: pea_apiexecution

ColumnData typePurpose
pea_namePrimary nameExecution identifier
pea_methodSingle line of textHTTP method
pea_resourceSingle line of textTarget resource
pea_statuscodeWhole numberHTTP response status
pea_successYes/NoExpected outcome matched
pea_durationmsWhole numberClient-observed duration
pea_correlationidSingle line of textDiagnostic correlation
pea_responsebodyMultiple lines of textSanitized response
pea_executedonDate and timeExecution 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/json
OData-Version: 4.0
OData-MaxVersion: 4.0
Authorization: Bearer <access_token>

Requests with JSON bodies also use:

Content-Type: application/json

Additional headers affect behavior.

HeaderPurpose
AuthorizationSupplies OAuth access token
AcceptSpecifies acceptable response representation
Content-TypeDescribes request body format
Prefer: return=representationRequests a representation after supported write operations
If-MatchSupports conditional updates
If-None-MatchSupports conditional create and cache semantics
Prefer: odata.maxpagesize=...Requests a page-size limit
Retry-AfterCommunicates 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_apirequests
Content-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_amount
Accept: application/json

The $select parameter restricts the returned columns.

6.3 Update

PATCH /api/data/v9.2/pea_apirequests(<record-guid>)
Content-Type: application/json
If-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:

eq
ne
gt
ge
lt
le
and
or

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:

  1. Recognize throttling responses.
  2. Respect the server-provided retry delay.
  3. Avoid aggressive retry loops.
  4. Limit concurrent requests.
  5. Record diagnostic information.
  6. 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.DataverseWebApiLab
cd APP006.DataverseWebApiLab
dotnet 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 outcomeGeneral handling
200 OKProcess returned data
201 CreatedProcess created representation
204 No ContentTreat as successful operation without a body
400 Bad RequestInspect request contract
401 UnauthorizedInvestigate authentication
403 ForbiddenInvestigate authorization
404 Not FoundInvestigate resource identity and permissions
412 Precondition FailedHandle concurrency conflict
429 Too Many RequestsRespect Retry-After
5xxEvaluate 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:

MetricPurpose
Total API executionsNumber of recorded operations
Successful requestsOperations matching expected outcomes
Failed requestsOperations with unexpected outcomes
Average durationMean client-observed response time
HTTP 429 countRecorded throttling responses
Latest executionMost 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.

ExperimentExpected behavior
Missing access tokenAuthentication failure
Invalid token audienceRequest rejected
Application user without required privilegeAuthorization failure
Invalid entity setResource error
Invalid column nameRequest validation error
Invalid JSONRequest rejected
Update missing record with If-Match: *No unintended record creation
Update using stale ETagConcurrency failure
Follow @odata.nextLinkRetrieve subsequent pages
Exceed service protection limitsHTTP 429 and retry guidance
Retry ambiguous POSTEvaluate 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

DimensionDataverse SDKDirect Web API
Programming model.NET object modelHTTP and JSON
AuthenticationSupported client abstractionsExplicit OAuth handling
Query constructionQueryExpression, FetchXML, LINQOData, FetchXML
HTTP visibilityAbstracted by clientDirectly observable
Retry behaviorDepends on SDK clientImplemented or configured by client
Cross-language supportPrimarily .NETAny HTTP-capable platform
Metadata operationsSDK requestsMetadata endpoints
ConcurrencySDK concurrency mechanismsETags 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

  1. Use the Microsoft Dataverse Web API — Web API architecture and capabilities.
  2. Authenticate to Microsoft Dataverse with the Web API — Authentication approaches.
  3. Use OAuth with Microsoft Dataverse — OAuth authentication architecture.
  4. Create a table row using the Web API — Create operations and response behavior.
  5. Query data using the Web API — OData queries.
  6. Page results using OData — Pagination.
  7. Perform conditional operations using the Web API — ETags and conditional requests.
  8. 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

Edvaldo Guimrães Filho Avatar

Published by