Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 94 additions & 3 deletions content/docs/analyzers/LinterCop/LC0031.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,21 @@ linkTitle = 'LC0031'
ignoreObsolete = true
+++

`LockTable()` is the traditional way to acquire an update lock before reading or modifying records. The problem is that it modifies **global session state**: once called on any record variable, every subsequent read against that table on *any* variable instance — acquires an update lock for the remainder of the transaction. An event subscriber, a FlowField calculation, or unrelated code further down the call stack can end up locking rows it never intended to touch.
`LockTable()` does not acquire a lock. It marks the table so that every subsequent read of that table in the transaction, on *any* record variable, uses the SQL `UPDLOCK` hint until the transaction commits. An event subscriber, a FlowField calculation, or unrelated code further down the call stack ends up locking rows it never intended to touch.

`ReadIsolation`, introduced in Business Central 2023 wave 1 (v22), sets the isolation level on the **specific record variable** only. Other variables of the same table remain unaffected, keeping lock scope tight and predictable.
> If `Record.LockTable` is called on an `Item` record, all reads against that table will be done with the `UPDLOCK` hint, not just the variable it was called on.

There is another reason to make the switch: **tri-state locking** (default from v25 onward) falls back to pessimistic two-state locking the moment `LockTable()` is called anywhere in the transaction. Replacing `LockTable()` with `ReadIsolation` preserves the tri-state benefits — fewer locks, higher concurrency, and fewer lock timeouts across the system.
— [Performance Articles for Developers](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/performance/performance-developer) on Microsoft Learn

`ReadIsolation`, introduced in Business Central 2023 wave 1 (v22), sets the isolation level on the **specific record variable** only. Other variables of the same table remain unaffected. The two are not equivalent, and that difference is the point: `ReadIsolation` keeps lock scope tight and predictable.

There is a second reason to make the switch. [Tri-state locking](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-tri-state-locking) (default from v25 onward) performs reads after writes optimistically, and a single `LockTable()` switches the table back to pessimistic locking for the rest of the transaction:

> Explicitly using the LockTable method in code maintains the same behavior, disabling optimistic reads.

— [Tri-state locking in database](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-tri-state-locking) on Microsoft Learn

Replacing `LockTable()` with `ReadIsolation` preserves the tri-state benefits: fewer locks, higher concurrency, and fewer lock timeouts across the system.

### Example

Expand Down Expand Up @@ -44,9 +54,90 @@ begin
end;
{{< /highlight >}}

### Convert or delete

Microsoft's own [coding guidance](https://github.com/microsoft/BCQuality/tree/main/microsoft/knowledge/performance) endorses `LockTable` for exactly one situation: a read that a directly following `Insert`, `Modify`, or `Delete` depends on. One question decides what to do with a flagged call:

- **A read of the same table follows, and a write depends on what it returned.** Convert: put `ReadIsolation(IsolationLevel::UpdLock)` on the instance that performs that read.
- **No such read follows.** The lock protects nothing. Delete the line.

The modern shape for "make sure nobody else took this number" is `ReadIsolation` on the instance that reads. The Base Application's `Customer` table does this in `OnInsert`:

{{< highlight al "hl_lines=4" >}}
var
Customer: Record Customer;
begin
Customer.ReadIsolation(IsolationLevel::ReadUncommitted);
while Customer.Get("No.") do
"No." := NoSeries.GetNextNo("No. Series");
{{< /highlight >}}

The same pattern appears in `Vendor`, `Item`, `Contact`, `Employee`, `Bank Account`, `Resource`, and in the Sustainability ESG tables added in 2025.

### LockTable in table triggers

A bare `LockTable()` in `OnInsert` or `OnDelete` binds to the same built-in method as `MyVar.LockTable()`: the compiler models no self-receiver special case, and the runtime effect is the same transaction-wide `UPDLOCK` on the table. The diagnostic is therefore reported inside triggers as well.

Most trigger `LockTable()` calls in the Base Application come in two shapes.

**No read follows.** A journal table locks itself in `OnInsert` and then reads an unrelated template table. The lock on the journal line table serves no read:

{{< highlight al "hl_lines=3" >}}
trigger OnInsert()
begin
LockTable(); // Use ReadIsolation instead of LockTable [LC0031]
ItemJnlTemplate.Get("Journal Template Name");
end;
{{< /highlight >}}

Delete the line. The Base Application removed exactly this call from `Item Journal Line` in version 26:

{{< highlight al >}}
trigger OnInsert()
begin
ItemJnlTemplate.Get("Journal Template Name");
end;
{{< /highlight >}}

**A dependent read follows.** An entry table locks itself in `OnInsert`, then a second instance reads the last number, and the insert depends on that value:

{{< highlight al "hl_lines=5" >}}
trigger OnInsert()
var
Attachment2: Record Attachment;
begin
Attachment2.LockTable(); // Use ReadIsolation instead of LockTable [LC0031]
if Attachment2.FindLast() then
"No." := Attachment2."No." + 1;
end;
{{< /highlight >}}

Convert the lock to `ReadIsolation` on the instance that reads:

{{< highlight al "hl_lines=5" >}}
trigger OnInsert()
var
Attachment2: Record Attachment;
begin
Attachment2.ReadIsolation(IsolationLevel::UpdLock);
if Attachment2.FindLast() then
"No." := Attachment2."No." + 1;
end;
{{< /highlight >}}

The Base Application still contains about 78 `LockTable()` calls in table triggers. Since version 23.5 it has removed two and added none, and the newer Microsoft apps (Business Foundation, E-Document Core, Subscription Billing, Excise Taxes) contain no `LockTable` at all.

### Code fix

The **ALCops: Replace LockTable() with ReadIsolation** code fix rewrites the call to `ReadIsolation(IsolationLevel::UpdLock)` on the same receiver: `MyVar.LockTable()` becomes `MyVar.ReadIsolation(...)`, `Rec.LockTable()` becomes `Rec.ReadIsolation(...)`, and a bare `LockTable()` inside a table or tableextension becomes a bare `ReadIsolation(...)`. It drops the `Wait` and `VersionCheck` arguments of `LockTable(true, true)`, because `ReadIsolation` has no equivalent.

The fix always converts and never deletes. Apply it when a dependent read follows; when the call protects nothing, delete the line instead.

### See also

- [Record instance isolation level](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-read-isolation) — Microsoft Learn reference for `ReadIsolation` and isolation levels
- [Prefer ReadIsolation over LockTable for reads](https://github.com/microsoft/BCQuality/blob/main/microsoft/knowledge/performance/prefer-readisolation-over-locktable-for-reads.md) and [Do not LockTable in a read-only procedure](https://github.com/microsoft/BCQuality/blob/main/microsoft/knowledge/performance/do-not-locktable-in-read-only-procedure.md) — Microsoft's BCQuality guidance on when `LockTable` is still justified
- [BC Internals: Tri-state locking](https://bcinternals.com/posts/tri-state-locking/) — The runtime team on the locking model, with the options listed in order of preference
- [Locking Scope: Differences between LockTable and ReadIsolation](https://www.keytogoodcode.com/post/locking-scope-differences-between-locktable-and-readisolation) — SQL-level analysis showing how `LockTable` leaks locks to unrelated code
- [Optimized Locking Feature vs Dynamics 365 Business Central](https://duiliotacconi.com/2025/05/30/optimized-locking-feature-vs-dynamics-365-business-central/) — Tri-state locking, RCSI, and transaction isolation in practice
- [Rec.LockTable: Good Practice or Bad Practice?](https://waldo.be/2024/03/28/rec-locktable-good-practice-or-bad-practice/) — How a long-standing best practice became an anti-pattern
Expand Down