Dataverse Optimistic Concurrency Laboratory: RowVersion, ETags, Lost Updates, and Conflict Resolution with C#

Engineering safe concurrent updates in Microsoft Dataverse through version-aware writes, SDK concurrency contracts, Web API conditional requests, and reproducible race-condition experiments.

Power Platform Engineering Academy — Five Years in Five Days

Application: APP-009 — Optimistic Concurrency Lab
Technical domain: Microsoft Dataverse
Technologies: C#, Dataverse SDK, Web API, RowVersion, ETags, Organization Service, Model-Driven Apps
Status: Pre-implementation engineering design — experiments not yet executed


Introduction

Concurrency defects are among the most dangerous problems in enterprise applications because they can occur without exceptions.

Two users may retrieve the same Dataverse record, modify different fields, and save their changes at different times.

Depending on the update payloads and business logic, one update may silently overwrite another.

The database can remain technically consistent while the business state becomes incorrect.

Consider a financial approval record with an amount of 1,000.

Two independent clients read the same version.

Client A increases the amount to 1,200.

Client B, working with stale data, changes the amount to 900.

If both clients update the same column without a version check, the final value can depend on the order in which the writes are processed.

The first successful update may be lost.

This is a classic lost-update anomaly.

Dataverse supports optimistic concurrency mechanisms that allow a client to detect when a record has changed since it was retrieved.

However, using these mechanisms correctly requires understanding record versions, request contracts, stale data, and conflict-resolution strategies.

APP-009 — Dataverse Optimistic Concurrency Laboratory explores these mechanisms through a C# concurrency harness, Dataverse record-version inspection, and a Model-Driven App.

Our central engineering question is:

How can we prevent silent overwrites when multiple clients attempt to modify the same Dataverse record concurrently?

1. Understanding the Lost-Update Problem

Imagine a record with the following initial state:

Record: APPROVAL-001
Amount: 1000
Version: V1

Two clients retrieve it.

             Dataverse
           Amount = 1000
            Version V1
             /      \
            /        \
       Client A     Client B
       Reads V1     Reads V1
           |            |
       Amount 1200   Amount 900
           |            |
        Update A      Update B
           |            |
           +-----+------+
                 |
          Final Amount?

Without a concurrency condition, both requests may be valid.

If A updates first and B updates second, the final amount may be 900.

Client A’s update has been overwritten.

The critical problem is not that Dataverse allowed two requests.

The problem is that neither request expressed the business requirement:

Only update this record if it has not changed since I read it.

Optimistic concurrency provides a way to express that requirement.

2. Optimistic Versus Pessimistic Concurrency

Two broad concurrency strategies exist.

StrategyMechanismTypical trade-off
Optimistic concurrencyDetect conflicting changes at write timeAvoids holding application-level locks during user interaction
Pessimistic concurrencyPrevent competing changes through lockingCan introduce blocking and contention

Dataverse exposes supported optimistic concurrency mechanisms through the SDK and Web API.

Developers should not assume they can acquire arbitrary SQL row locks or control database isolation levels directly.

The practical engineering model is:

  1. Retrieve a record and its version.
  2. Modify the intended values.
  3. Submit the update with a version condition.
  4. Accept the update only if the version still matches.
  5. Resolve the conflict if the version has changed.

3. Dataverse Concurrency Contracts

Dataverse supports version-aware operations through the Organization Service and Web API.

3.1 SDK RowVersion

The SDK Entity class exposes:

entity.RowVersion

This value represents a version token associated with the retrieved record.

The token should be treated as an opaque value.

Developers should not assume its numeric representation can be used to calculate business timestamps, elapsed time, or the number of changes.

3.2 SDK ConcurrencyBehavior

The SDK supports ConcurrencyBehavior values such as:

ConcurrencyBehavior.Default
ConcurrencyBehavior.IfRowVersionMatches
ConcurrencyBehavior.AlwaysOverwrite

For version-aware updates, the client uses:

ConcurrencyBehavior.IfRowVersionMatches

The request is accepted only when the supplied row version matches the current record version, subject to table support and the operation contract.

3.3 Web API ETags

The Web API exposes record versions through OData ETags.

An example response might include:

{
"@odata.etag": "W/\"1234567\"",
"pea_name": "Approval 001",
"pea_amount": 1000.00
}

The client can use the ETag with an HTTP conditional request.

PATCH /api/data/v9.2/pea_concurrencyrecords(<record-guid>)
If-Match: W/"1234567"
Content-Type: application/json
{
"pea_amount": 1200.00
}

If the record version has changed, the update can be rejected with HTTP 412 Precondition Failed.

Reference: Perform conditional operations using the Web API.

4. Application Architecture

APP-009 Optimistic Concurrency Laboratory
│
├── C# Concurrency Runner
│ ├── Retrieve Record Versions
│ ├── Simulate Two Clients
│ ├── Unconditional Updates
│ ├── Conditional Updates
│ ├── Conflict Detection
│ ├── Retry Strategies
│ └── Result Verification
│
├── Dataverse
│ ├── pea_concurrencyrecord
│ ├── pea_concurrencyattempt
│ └── pea_concurrencyscenario
│
└── Model-Driven App
├── Entry Dashboard
├── Concurrency Records
├── Update Attempts
├── Conflict Diagnostics
└── Scenario Configuration

The C# application controls competing update attempts.

Dataverse stores the records and diagnostic evidence.

The Model-Driven App exposes the resulting state and allows engineers to inspect conflicts.

The objective is to make concurrency failures reproducible rather than rely on unpredictable user activity.

5. Dataverse Data Model

5.1 Concurrency Record

Logical name: pea_concurrencyrecord

ColumnTypePurpose
pea_namePrimary nameRecord identifier
pea_externalcodeSingle line of textExperiment correlation
pea_amountDecimal numberConcurrently modified value
pea_descriptionMultiple lines of textAdditional test field
pea_lastwriterSingle line of textLast logical client
pea_lastupdatedDate and timeExperiment timestamp

The table must support optimistic concurrency.

The laboratory will inspect the table’s metadata to verify this capability before running conditional updates.

5.2 Concurrency Attempt

Logical name: pea_concurrencyattempt

ColumnTypePurpose
pea_namePrimary nameAttempt identifier
pea_scenarioSingle line of textScenario name
pea_clientSingle line of textLogical client A or B
pea_recordidSingle line of textTarget GUID
pea_readversionSingle line of textVersion read before update
pea_successYes/NoWhether update succeeded
pea_conflictYes/NoWhether conflict was detected
pea_attemptedamountDecimal numberRequested value
pea_durationmsWhole numberClient-observed duration
pea_messageMultiple lines of textDiagnostic details
pea_executedonDate and timeAttempt timestamp

5.3 Concurrency Scenario

Logical name: pea_concurrencyscenario

ColumnTypePurpose
pea_namePrimary nameScenario name
pea_strategyChoiceUnconditional or conditional
pea_descriptionMultiple lines of textExperiment objective
pea_expectedconflictsWhole numberExpected conflict count
pea_enabledYes/NoScenario availability

The data model separates business state from experiment diagnostics.

This distinction matters because failed conditional updates should still produce observable diagnostic records.

6. Inspecting Concurrency Metadata

Before using version-aware updates, the application should inspect the table metadata.

The relevant metadata property is:

EntityMetadata.IsOptimisticConcurrencyEnabled

The experiment can retrieve it using RetrieveEntityRequest.

var request = new RetrieveEntityRequest
{
LogicalName = "pea_concurrencyrecord",
EntityFilters = EntityFilters.Entity
};
var response = (RetrieveEntityResponse)
service.Execute(request);
bool enabled =
response.EntityMetadata
.IsOptimisticConcurrencyEnabled == true;

If the table does not support optimistic concurrency, the experiment must not assume that conditional updates will behave as expected.

The metadata check is therefore a prerequisite.

Reference: Optimistic concurrency.

7. Experiment A: Unconditional Updates

The first experiment deliberately omits version checking.

Two logical clients retrieve the same record.

Entity clientA = service.Retrieve(
"pea_concurrencyrecord",
recordId,
new ColumnSet("pea_amount"));
Entity clientB = service.Retrieve(
"pea_concurrencyrecord",
recordId,
new ColumnSet("pea_amount"));

Client A updates the amount to 1,200.

var updateA = new Entity(
"pea_concurrencyrecord",
recordId);
updateA["pea_amount"] = 1200m;
service.Update(updateA);

Client B then updates the amount to 900.

var updateB = new Entity(
"pea_concurrencyrecord",
recordId);
updateB["pea_amount"] = 900m;
service.Update(updateB);

Both operations may succeed.

The final amount is expected to be 900 when the updates execute in this order.

This demonstrates the lost-update problem.

The experiment uses deterministic sequential writes from stale snapshots before introducing actual parallel execution.

That allows the failure mechanism to be isolated without scheduling uncertainty.

8. Experiment B: Conditional Updates

The second experiment retrieves the same record twice.

Both clients receive the same initial row version.

Client A updates the record using IfRowVersionMatches.

var updateA = new Entity(
"pea_concurrencyrecord",
recordId)
{
RowVersion = clientA.RowVersion
};
updateA["pea_amount"] = 1200m;
var requestA = new UpdateRequest
{
Target = updateA,
ConcurrencyBehavior =
ConcurrencyBehavior.IfRowVersionMatches
};
service.Execute(requestA);

After A succeeds, the record version changes.

Client B attempts to update using its stale version.

var updateB = new Entity(
"pea_concurrencyrecord",
recordId)
{
RowVersion = clientB.RowVersion
};
updateB["pea_amount"] = 900m;
var requestB = new UpdateRequest
{
Target = updateB,
ConcurrencyBehavior =
ConcurrencyBehavior.IfRowVersionMatches
};
service.Execute(requestB);

The second request should fail with a concurrency-related service fault.

The final amount should remain 1,200.

This is the fundamental behavior the laboratory must verify.

9. Why Conflict Detection Is Not Conflict Resolution

Optimistic concurrency prevents silent overwrites, but it does not decide which change is correct.

When a conflict occurs, the application must choose a policy.

Possible strategies include:

StrategyBehaviorRisk
Reject and notifyInform user that data changedRequires user intervention
Refresh and retryReload current state and recomputeMay change business intent
Field-level mergeMerge nonconflicting changesRequires precise field semantics
Last writer winsIgnore conflict and overwriteCan lose business changes
Business arbitrationApply domain-specific rulesRequires explicit decision logic

The correct policy depends on the business domain.

For example, automatically retrying a description update may be acceptable in some applications.

Automatically overwriting a financial approval amount may not be acceptable.

The laboratory will not assume that retrying is always the correct response.

10. Independent C# Concurrency Laboratory

The proposed project is:

PowerPlatformEngineeringAcademy/
└── Dataverse/
└── APP009.OptimisticConcurrencyLab/
├── APP009.OptimisticConcurrencyLab.csproj
└── Program.cs

Create the project:

dotnet new console --name APP009.OptimisticConcurrencyLab
cd APP009.OptimisticConcurrencyLab
dotnet add package Microsoft.PowerPlatform.Dataverse.Client

The program will:

  1. Authenticate to Dataverse.
  2. Verify optimistic concurrency support.
  3. Create a test record.
  4. Execute unconditional stale updates.
  5. Reset the record.
  6. Execute conditional stale updates.
  7. Capture the expected conflict.
  8. Verify the final persisted amount.
  9. Record each attempt.
  10. Report unexpected outcomes.

The following implementation uses controlled sequential writes to guarantee stale-version conditions.

Actual parallel execution will be introduced as a separate experiment.

11. Complete C# Concurrency Runner

The following implementation creates two controlled scenarios:

  • Unconditional: Both clients update a record using stale snapshots, without version checking.
  • Conditional: Both clients use the original row version, but the second update must be rejected after the first changes the record.

The harness records individual update attempts and verifies the persisted amount after each scenario.

The expected SDK conflict code is -2147088254 (ConcurrencyVersionMismatch), as documented by Microsoft. <Cite refs={[“turn430701search0″,”turn430701search2”]}/>

using System;
using System.Diagnostics;
using System.ServiceModel;
using Microsoft.Crm.Sdk.Messages;
using Microsoft.PowerPlatform.Dataverse.Client;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Messages;
using Microsoft.Xrm.Sdk.Metadata;
using Microsoft.Xrm.Sdk.Query;
const string RecordTable = "pea_concurrencyrecord";
const string AttemptTable = "pea_concurrencyattempt";
const int VersionMismatch = -2147088254;
string connectionString =
Environment.GetEnvironmentVariable("DATAVERSE_CONNECTION")
?? throw new InvalidOperationException(
"Missing DATAVERSE_CONNECTION.");
using var client = new ServiceClient(connectionString);
if (!client.IsReady)
throw new InvalidOperationException(
$"Connection failed: {client.LastError}");
IOrganizationService service = client;
// Verify that the table supports optimistic concurrency.
var metadataResponse = (RetrieveEntityResponse)service.Execute(
new RetrieveEntityRequest
{
LogicalName = RecordTable,
EntityFilters = EntityFilters.Entity
});
if (metadataResponse.EntityMetadata
.IsOptimisticConcurrencyEnabled != true)
{
throw new InvalidOperationException(
"Optimistic concurrency is not enabled.");
}
Guid CreateTestRecord(string scenario)
{
var record = new Entity(RecordTable)
{
["pea_name"] = $"APP009 {scenario}",
["pea_externalcode"] =
$"APP009-{Guid.NewGuid():N}",
["pea_amount"] = 1000m,
["pea_lastwriter"] = "Initializer",
["pea_lastupdated"] = DateTime.UtcNow
};
return service.Create(record);
}
Entity ReadSnapshot(Guid id)
{
Entity record = service.Retrieve(
RecordTable,
id,
new ColumnSet("pea_amount", "pea_lastwriter"));
if (string.IsNullOrWhiteSpace(record.RowVersion))
throw new InvalidOperationException(
"Retrieved RowVersion is missing.");
return record;
}
void LogAttempt(
string scenario,
string logicalClient,
Guid recordId,
string? readVersion,
decimal amount,
bool success,
bool conflict,
long durationMs,
string message)
{
var log = new Entity(AttemptTable)
{
["pea_name"] = $"APP009-{Guid.NewGuid():N}",
["pea_scenario"] = scenario,
["pea_client"] = logicalClient,
["pea_recordid"] = recordId.ToString(),
["pea_readversion"] = readVersion ?? "",
["pea_success"] = success,
["pea_conflict"] = conflict,
["pea_attemptedamount"] = amount,
["pea_durationms"] = checked((int)durationMs),
["pea_message"] = message,
["pea_executedon"] = DateTime.UtcNow
};
service.Create(log);
}
bool UpdateRecord(
string scenario,
string logicalClient,
Entity snapshot,
decimal newAmount,
bool conditional)
{
var stopwatch = Stopwatch.StartNew();
bool success = false;
bool conflict = false;
string message = "";
try
{
var update = new Entity(RecordTable, snapshot.Id)
{
["pea_amount"] = newAmount,
["pea_lastwriter"] = logicalClient,
["pea_lastupdated"] = DateTime.UtcNow
};
if (conditional)
{
update.RowVersion = snapshot.RowVersion;
var request = new UpdateRequest
{
Target = update,
ConcurrencyBehavior =
ConcurrencyBehavior.IfRowVersionMatches
};
service.Execute(request);
}
else
{
service.Update(update);
}
success = true;
message = "Update accepted";
}
catch (FaultException<OrganizationServiceFault> ex)
when (ex.Detail.ErrorCode == VersionMismatch)
{
conflict = true;
message = $"Version conflict: {ex.Detail.ErrorCode}";
}
catch (FaultException<OrganizationServiceFault> ex)
{
message =
$"Unexpected Dataverse fault: {ex.Detail.ErrorCode}";
throw;
}
finally
{
stopwatch.Stop();
LogAttempt(
scenario,
logicalClient,
snapshot.Id,
snapshot.RowVersion,
newAmount,
success,
conflict,
stopwatch.ElapsedMilliseconds,
message);
}
Console.WriteLine(
$"{scenario}/{logicalClient}: " +
$"Success={success}, Conflict={conflict}");
return conflict;
}
decimal ReadAmount(Guid id)
{
Entity record = service.Retrieve(
RecordTable,
id,
new ColumnSet("pea_amount"));
return record.GetAttributeValue<decimal>(
"pea_amount");
}
// SCENARIO A: UNCONDITIONAL UPDATES
Guid recordA = CreateTestRecord("Unconditional");
Entity snapshotA1 = ReadSnapshot(recordA);
Entity snapshotA2 = ReadSnapshot(recordA);
UpdateRecord(
"Unconditional",
"Client A",
snapshotA1,
1200m,
conditional: false);
UpdateRecord(
"Unconditional",
"Client B",
snapshotA2,
900m,
conditional: false);
decimal finalA = ReadAmount(recordA);
Console.WriteLine($"Unconditional final amount: {finalA}");
if (finalA != 900m)
throw new InvalidOperationException(
$"Expected 900, observed {finalA}");
// SCENARIO B: CONDITIONAL UPDATES
Guid recordB = CreateTestRecord("Conditional");
Entity snapshotB1 = ReadSnapshot(recordB);
Entity snapshotB2 = ReadSnapshot(recordB);
UpdateRecord(
"Conditional",
"Client A",
snapshotB1,
1200m,
conditional: true);
bool conflictDetected = UpdateRecord(
"Conditional",
"Client B",
snapshotB2,
900m,
conditional: true);
decimal finalB = ReadAmount(recordB);
Console.WriteLine($"Conditional final amount: {finalB}");
if (!conflictDetected || finalB != 1200m)
{
throw new InvalidOperationException(
"Optimistic concurrency assertion failed.");
}
Console.WriteLine(
"Concurrency laboratory completed.");

11.1 Expected results

ScenarioClient AClient BFinal amount
UnconditionalUpdate succeedsUpdate succeeds900
ConditionalUpdate succeedsVersion conflict1,200

These results are expected, not yet observed.

The laboratory must be executed against Dataverse and the records inspected through the Model-Driven App before confirming the behavior.

11.2 Implementation limitations

The program simulates competing clients through sequential writes from previously retrieved snapshots.

This is sufficient to demonstrate stale-version behavior, but it does not measure genuine parallel contention.

The diagnostic records are written after each update attempt, using independent service calls. Logging failures can therefore interrupt the experiment.

The program also leaves test records in Dataverse intentionally so they can be inspected in the Model-Driven App.

A later implementation should introduce cleanup and retention policies.

12. Web API Concurrency Using ETags

The same principle can be implemented through the Dataverse Web API.

First, retrieve the record:

GET /api/data/v9.2/pea_concurrencyrecords(<guid>)?$select=pea_amount
Accept: application/json

The response includes an ETag representing the retrieved version.

The client then submits:

PATCH /api/data/v9.2/pea_concurrencyrecords(<guid>)
If-Match: W/"1234567"
Content-Type: application/json
{
"pea_amount": 1200.00
}

If the record version still matches, the update succeeds.

If the version has changed, Dataverse rejects the operation with HTTP 412 Precondition Failed. <Cite ref=”turn430701search4″/>

A key distinction is that:

If-Match: *

is not equivalent to:

If-Match: W/"1234567"

The wildcard is commonly used to require that a record already exists, preventing an unintended Upsert.

An exact ETag adds version checking.

This is why blindly adding If-Match: * to every PATCH request does not solve lost-update problems.

13. Optimistic Concurrency on Delete

Concurrency protection is not limited to updates.

The SDK also supports conditional deletes.

var request = new DeleteRequest
{
Target = new EntityReference(
"pea_concurrencyrecord",
recordId),
ConcurrencyBehavior =
ConcurrencyBehavior.IfRowVersionMatches
};
request.Target.RowVersion = retrieved.RowVersion;
service.Execute(request);

If the record changed after the original read, the delete should be rejected rather than silently remove a newer version.

The Web API provides an equivalent pattern:

DELETE /api/data/v9.2/pea_concurrencyrecords(<guid>)
If-Match: W/"1234567"

A stale ETag produces a precondition failure instead of deleting the changed record. <Cite ref=”turn430701search3″/>

This is especially important for operations that remove records containing recent work by other users.

14. Concurrency and Transactions Are Different Problems

APP-008 investigated transaction atomicity.

APP-009 investigates conflicting writes.

These are related but distinct concerns.

ConcernTransaction atomicityOptimistic concurrency
Central questionDo related changes commit together?Has the record changed since it was read?
Primary failurePartial business operationLost or conflicting update
SDK mechanismExecuteTransactionRequestIfRowVersionMatches
Web API mechanismTransactional change setExact ETag with If-Match
Typical resolutionRollbackReject, refresh, merge, or retry

A transaction does not automatically prevent a business-level lost update when a client bases its update on stale data retrieved earlier.

Likewise, optimistic concurrency on one record does not make several independent operations atomic.

Enterprise systems often require both mechanisms.

15. Actual Parallel Execution

After validating deterministic stale snapshots, the laboratory should introduce real concurrency.

A controlled test can use two independent ServiceClient instances and a synchronization barrier.

Both workers retrieve the same record before either is allowed to update it.

Worker A Worker B
| |
Retrieve V1 Retrieve V1
| |
+-------- Barrier ----------+
| |
Update with V1 Update with V1
| |
Success or conflict Success or conflict

The expected result under conditional updates is that at most one update using the shared version succeeds.

The experiment must verify the number of successful writes, the number of conflicts, and the final persisted value.

It should not depend on which worker wins.

The order is nondeterministic.

16. Conflict Resolution Engineering

A robust application needs more than conflict detection.

Strategy A — Reject and notify

The application informs the user that the record changed and requests a refresh.

This is appropriate when business decisions require human review.

Strategy B — Refresh and recompute

The application retrieves the latest state and recalculates the intended update.

This is suitable when the operation is deterministic and safe to repeat.

Strategy C — Field-level merge

The application compares the original snapshot, current server state, and proposed changes.

It then merges changes when business rules permit.

This requires more than checking whether two clients changed different fields.

Plug-ins, calculated values, business rules, and related records may introduce dependencies.

Strategy D — Explicit overwrite

The application intentionally ignores the stale version and overwrites the current value.

This should be a deliberate business decision, not an accidental consequence of using the default update behavior.

17. Model-Driven App: PEA Optimistic Concurrency Laboratory

The operational application will be named:

PEA Optimistic Concurrency Laboratory

Entry dashboard

MetricPurpose
Total update attemptsNumber of attempted writes
Successful updatesAccepted writes
Version conflictsRejected stale-version writes
Conflict rateConflicts divided by relevant attempts
Latest conflictMost recent detected conflict
Active scenariosAvailable experiments

Concurrency Records

The form displays the current amount, last writer, last update time, and related attempt history.

Update Attempts

The view displays the client identity, attempted value, retrieved version, success flag, conflict flag, and execution duration.

Scenario Configuration

The scenario form documents whether the experiment uses unconditional updates, conditional updates, or a conflict-resolution strategy.

Conflict Diagnostics

The diagnostic view allows engineers to compare competing attempts against the same record.

A custom page may later provide a side-by-side display of original, current, and proposed values.

18. Failure Injection Matrix

ExperimentExpected observation
Two unconditional stale updatesLater write can overwrite earlier value
Two conditional stale updatesOne update rejected
Missing RowVersionVersion-not-provided fault
Invalid RowVersionVersion-mismatch fault
Unsupported concurrency configurationPlatform fault
Stale ETag in Web API PATCHHTTP 412
Stale ETag in Web API DELETEHTTP 412
Wildcard If-MatchExistence check, not exact version protection
Parallel conditional updatesAt most one succeeds using the shared version
Blind retry after conflictPotential business-level overwrite

Microsoft documents distinct SDK fault codes for version mismatch, missing row version, and disabled optimistic concurrency. <Cite ref=”turn430701search0″/>

19. Performance and Architectural Trade-offs

Optimistic concurrency avoids holding an application-level lock while a user edits a record.

However, it introduces the possibility of rejected writes.

In low-contention workloads, this can be efficient.

In high-contention workloads, repeated conflicts and retries can become expensive.

A useful benchmark should measure:

  • Successful updates per second.
  • Conflict rate.
  • Average update latency.
  • Retry count.
  • Number of refreshed reads.
  • Final-state correctness.

The objective is not to eliminate every conflict.

The objective is to ensure that conflicts do not silently corrupt business state.

20. Microsoft Learn Technical References

  1. Optimistic concurrency — Supported tables, SDK behavior, and concurrency fault codes.
  2. Perform conditional operations using the Web API — ETags, conditional updates, and conditional deletes.
  3. Web API Conditional Operations Sample (C#) — Official conditional-request examples.
  4. Web service error codes — Dataverse service fault definitions.
  5. Use the Dataverse SDK for .NET — SDK service contracts.
  6. Execute messages in a single database transaction — Transactional operations and rollback.

Conclusion

Optimistic concurrency is a fundamental mechanism for protecting Dataverse records against silent lost updates.

The SDK exposes version-aware operations through RowVersion and ConcurrencyBehavior.IfRowVersionMatches.

The Web API exposes the same architectural principle through ETags and conditional HTTP requests.

Neither mechanism automatically resolves a business conflict.

It detects that the assumptions behind a proposed write are no longer valid.

The application must then decide whether to reject, refresh, merge, retry, or deliberately overwrite.

The central engineering principle is:

A write based on stale state must not silently replace newer business data when correctness depends on the version originally read.

APP-009 establishes the foundation for more advanced investigations into Dataverse security, plug-in concurrency, distributed integration, and enterprise consistency patterns.


Power Platform Engineering Academy — Five Years in Five Days

Article 009/100 — Optimistic Concurrency Laboratory

Read the version. Submit the condition. Detect the conflict. Preserve business intent.

Edvaldo Guimrães Filho Avatar

Published by