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-001Amount: 1000Version: 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.
| Strategy | Mechanism | Typical trade-off |
|---|---|---|
| Optimistic concurrency | Detect conflicting changes at write time | Avoids holding application-level locks during user interaction |
| Pessimistic concurrency | Prevent competing changes through locking | Can 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:
- Retrieve a record and its version.
- Modify the intended values.
- Submit the update with a version condition.
- Accept the update only if the version still matches.
- 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.DefaultConcurrencyBehavior.IfRowVersionMatchesConcurrencyBehavior.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
| Column | Type | Purpose |
|---|---|---|
pea_name | Primary name | Record identifier |
pea_externalcode | Single line of text | Experiment correlation |
pea_amount | Decimal number | Concurrently modified value |
pea_description | Multiple lines of text | Additional test field |
pea_lastwriter | Single line of text | Last logical client |
pea_lastupdated | Date and time | Experiment 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
| Column | Type | Purpose |
|---|---|---|
pea_name | Primary name | Attempt identifier |
pea_scenario | Single line of text | Scenario name |
pea_client | Single line of text | Logical client A or B |
pea_recordid | Single line of text | Target GUID |
pea_readversion | Single line of text | Version read before update |
pea_success | Yes/No | Whether update succeeded |
pea_conflict | Yes/No | Whether conflict was detected |
pea_attemptedamount | Decimal number | Requested value |
pea_durationms | Whole number | Client-observed duration |
pea_message | Multiple lines of text | Diagnostic details |
pea_executedon | Date and time | Attempt timestamp |
5.3 Concurrency Scenario
Logical name: pea_concurrencyscenario
| Column | Type | Purpose |
|---|---|---|
pea_name | Primary name | Scenario name |
pea_strategy | Choice | Unconditional or conditional |
pea_description | Multiple lines of text | Experiment objective |
pea_expectedconflicts | Whole number | Expected conflict count |
pea_enabled | Yes/No | Scenario 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:
| Strategy | Behavior | Risk |
|---|---|---|
| Reject and notify | Inform user that data changed | Requires user intervention |
| Refresh and retry | Reload current state and recompute | May change business intent |
| Field-level merge | Merge nonconflicting changes | Requires precise field semantics |
| Last writer wins | Ignore conflict and overwrite | Can lose business changes |
| Business arbitration | Apply domain-specific rules | Requires 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.OptimisticConcurrencyLabcd APP009.OptimisticConcurrencyLabdotnet add package Microsoft.PowerPlatform.Dataverse.Client
The program will:
- Authenticate to Dataverse.
- Verify optimistic concurrency support.
- Create a test record.
- Execute unconditional stale updates.
- Reset the record.
- Execute conditional stale updates.
- Capture the expected conflict.
- Verify the final persisted amount.
- Record each attempt.
- 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 UPDATESGuid 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 UPDATESGuid 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
| Scenario | Client A | Client B | Final amount |
|---|---|---|---|
| Unconditional | Update succeeds | Update succeeds | 900 |
| Conditional | Update succeeds | Version conflict | 1,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_amountAccept: 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.
| Concern | Transaction atomicity | Optimistic concurrency |
|---|---|---|
| Central question | Do related changes commit together? | Has the record changed since it was read? |
| Primary failure | Partial business operation | Lost or conflicting update |
| SDK mechanism | ExecuteTransactionRequest | IfRowVersionMatches |
| Web API mechanism | Transactional change set | Exact ETag with If-Match |
| Typical resolution | Rollback | Reject, 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
| Metric | Purpose |
|---|---|
| Total update attempts | Number of attempted writes |
| Successful updates | Accepted writes |
| Version conflicts | Rejected stale-version writes |
| Conflict rate | Conflicts divided by relevant attempts |
| Latest conflict | Most recent detected conflict |
| Active scenarios | Available 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
| Experiment | Expected observation |
|---|---|
| Two unconditional stale updates | Later write can overwrite earlier value |
| Two conditional stale updates | One update rejected |
| Missing RowVersion | Version-not-provided fault |
| Invalid RowVersion | Version-mismatch fault |
| Unsupported concurrency configuration | Platform fault |
| Stale ETag in Web API PATCH | HTTP 412 |
| Stale ETag in Web API DELETE | HTTP 412 |
Wildcard If-Match | Existence check, not exact version protection |
| Parallel conditional updates | At most one succeeds using the shared version |
| Blind retry after conflict | Potential 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
- Optimistic concurrency — Supported tables, SDK behavior, and concurrency fault codes.
- Perform conditional operations using the Web API — ETags, conditional updates, and conditional deletes.
- Web API Conditional Operations Sample (C#) — Official conditional-request examples.
- Web service error codes — Dataverse service fault definitions.
- Use the Dataverse SDK for .NET — SDK service contracts.
- 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.
