Treating the third visible grid row as the third stored record creates a bug that often stays hidden until someone sorts the list. Filtering or moving to another page changes the display order, while record identity stays the same. In a bulk editing screen, such as score entry, identity needs to be part of the request contract rather than inferred from presentation.
A row index describes the view
The score entry pages in my archive have separate listing, sorting and save handlers. This independent .NET 10 console example replaces persistence with a dictionary containing two records. It submits the edit for 205 before the edit for 101, making it easy to check that updates follow identities rather than array positions.
var stored = new Dictionary<int, decimal?> { [101] = null, [205] = 40m };
var edits = new[] { (Id:205, Score:(decimal?)70m), (Id:101, Score:(decimal?)0m) };
foreach (var edit in edits)
{
if (!stored.ContainsKey(edit.Id)) throw new ArgumentException("Unknown record");
if (edit.Score is < 0 or > 100) throw new ArgumentException("Out of range");
}
foreach (var edit in edits) stored[edit.Id] = edit.Score;
Console.WriteLine($"101={stored[101]};205={stored[205]}");
The outcome is 205=70 and 101=0 regardless of submission order. A WebForms GridView can carry the same identity through DataKeyNames and DataKeys instead of reading it from a visible cell or treating the row index as an ID. A browser-supplied identifier is still untrusted input, however. A hidden field does not create an authorization boundary.
Why validate before applying the edits?
The first loop validates the complete request. An unknown ID or out-of-range score causes an error before any assignment runs. Only after that pass succeeds does the second loop modify the dictionary. A valid first edit followed by an invalid second edit therefore does not leave the in-memory example partially changed.
Two loops do not create a database transaction. A connection failure, constraint violation or concurrent writer can intervene between real persistence operations. The service should validate the editable scope, write within its transaction and check the intended concurrency condition. A preflight check is not a guarantee that the database will remain unchanged until commit.
Keep an absent score distinct from a zero score
The nullable decimal has a purpose: null can mean not entered, while zero can be an intentional score. Converting every empty text field to zero destroys that distinction. The sample range expression does not reject null. If every record must have a score before submission, that requires an explicit additional rule; nullable comparison behavior should not decide the workflow accidentally.
Submitting the same ID twice makes the later assignment win in this demonstration. In a real bulk request I would normally reject duplicate IDs because the intended edit is ambiguous. Field names should also preserve the distinction between a current value, proposed value and expected version when concurrency is part of the contract.
Existing does not mean editable
A server needs more than a ContainsKey-style existence check. The record must belong to the permitted period, screen scope and user permissions. Knowing an ID should never be enough to modify it. Loading the submitted identities through one authorized query also gives a clearer validation flow than issuing an unrelated database lookup for every row.
A form that remains open for a long time benefits from a version check. If one user loaded a score of forty and another changed it to sixty, silently accepting seventy from the first user's stale form may be undesirable. An explicit conflict response lets the editor retain the proposed value while asking the user to review the current record.
Display success after persistence succeeds
Changing a selection box or local model is not a completed save. Prevent duplicate submissions while a request is running, and preserve the draft if the server rejects it. I would test reordered identities, valid zero, null, an out-of-range value, a duplicate ID and a record outside the authorized scope. For the successful case, reopening the screen should load the same values from persistence rather than merely reuse an optimistic local state.