Fix null document references in details/ modify/ delete/ print pages
Initial prompt to Codex
There is an error which occurs under the following sceanrio. Suppose I am on a voucher whose number is 1000. I come to the index of vouchers and open the voucher details. Some other user also is viewing the same voucher. Now I delete this voucher and return to the index. Now the other user is trying to modify something say amount. He gets an error because the id is no longer available. In the log we see a WRN stating that there is no json output for the requested id. This issue is there in every details/modify/print pages. How can we address this? Could you propose a plan? You could query codegraph if required. Similar issue is in many other places like invoice, bill, grn, orders, indents and so on
Assistant response
Yes. This is a shared stale-record problem, not a voucher-only bug.
What I’d do:
-
Fix the API contract for missing records.
- Any
Details/Modify/Printendpoint that loads by id should return404 NotFound()when the record no longer exists, instead of returningOk(null)or letting a null flow downstream. - CodeGraph shows the current pattern in
VoucherControllerandQrPrintController: they call repository methods and always wrap the result inOk(...), with no missing-record branch.
- Any
-
Handle “record deleted by someone else” in the UI.
- If the client gets
404, redirect back to the index/list page. - Show a clear message like: “This voucher was deleted by another user. The page has been closed.”
- Do this once in the shared UI data-loading path if possible, not page by page.
- If the client gets
-
Apply the same fix in the shared repository/controller pattern used by voucher, invoice, bill, GRN, orders, indents, etc.
- The right place is the common “get by id” path, not each button handler.
- That keeps the diff small and prevents the same bug from surviving in sibling pages.
-
For print pages, fail fast before generating anything.
- If the backing document is gone, return
404immediately. - Do not proceed to PDF generation with a missing source row.
- If the backing document is gone, return
-
Optional but worthwhile: add a lightweight concurrency check for edits.
- If you already have a last-updated timestamp, rowversion, or similar, use it to detect stale edits.
- That gives a better message when the user is editing an outdated screen, but it is secondary to the missing-record fix.
Suggested implementation order:
- First: identify the common “get by id” methods for all affected modules.
- Second: change them to return
NotFound()on missing data. - Third: update the frontend error handling for
404. - Fourth: verify with one deleted-record scenario per document type.
If you want, I can trace the exact shared methods for voucher/invoice/bill/grn/order/indent and propose the smallest code change path.
Deepseek review points
Fixing the API contract is the foundational change. Currently Ok(null) conveys “success, here’s your null record” — semantically wrong. NotFound() is the correct HTTP response for a missing resource by id. Every other fix depends on this.
Centralizing in the service layer is the right level — not as broad as a global handler (false positives on legitimate 404s), not as narrow as per-page try/catch. The service layer is where the HTTP→domain boundary lives.
What needs more thought
- GetFromJsonAsync throws on 404, not returns null.
The current ?? new T() only masks Ok(null). Once controllers return NotFound(), GetFromJsonAsync
A helper extension method would keep it DRY:
public static async Task<T?> GetOrNullAsync
The service returning null (instead of new T()) means every Details/Modify page needs a null guard in OnInitializedAsync before accessing properties:
_Voucher = await _IVoucherService.VoucherDetails(id, dbname); if (_Voucher == null) { /* snackbar + redirect */ return; } This is ~3 lines per page — simple and mechanical — but not zero. The existing pages already have role-check redirects (NavManager.NavigateTo("/accessdenied/1")), so the pattern is familiar.
- Modify endpoints need their own handling.
The VoucherModify controller (VoucherController.cs:1314) calls VoucherDetails(id).Result internally for audit trail. When the record is deleted, _OldValues is null and DetectChanges(null, …) throws NRE. This is a separate fix from the Details endpoint — modify controllers need an explicit existence check in the controller, not just in the Details GET endpoint.
- Print endpoints.
Same pattern — the VoucherPrint controller action (VoucherController.cs:1674) takes data from the client’s model rather than re-fetching, so it won’t benefit from the Details fix alone. Print controllers need their own null check.
Feasibility verdict Yes, feasible. The work breakdown:
Layer Change Count Pattern Controllers: Details endpoints return Ok(…) → return data != null ? Ok(data) : NotFound() ~100 endpoints, ~30 files One-liner mechanical change Controllers: Modify endpoints Add existence check before audit trail ~50 endpoints 3-5 line addition Controllers: Print endpoints Null check before PDF gen ~15 endpoints 3-5 line addition Web Services: GET methods Return nullable, use GetOrNullAsync helper ~100 methods 1-line changes with helper Web Services: modify methods Handle 404 from PUT, return bool/nullable ~50 methods 3-5 line changes Razor Pages Null guard + redirect ~100 pages 3-line addition each The helper extension method for GetOrNullAsync is what makes the service-layer changes DRY rather than tedious. Without it, you’d have try-catch boilerplate in every service method.
Rejoinder
Rejoinder:
Deepseek is right on the core contract: Ok(null) is the wrong shape for a deleted/missing document. NotFound() is the correct API response.
It is also right that the web service boundary is the right place to normalize HTTP failure into domain behavior.
Where I’d amend the plan is scope and mechanics:
-
GetFromJsonAsyncis the wrong primitive once the API returns404.- The current
?? new T()pattern only works if the server returns200 null. - After switching controllers to
NotFound(), GET callers need a shared helper that returnsnullon 404. - I would not do try/catch in every service method. One helper is the right lazy move.
- The current
-
Not every razor page needs bespoke logic, but each shared page pattern does need one null guard.
- If a page already loads its model via a service call in
OnInitializedAsyncor equivalent, it needs a single guard there. - That is still a lot of pages, but it is mechanical and can likely be rolled out by document family, not individually hand-crafted.
- The important point: this is a UI-layer concern, but it should be handled with a shared pattern, not bespoke per page.
- If a page already loads its model via a service call in
-
Modify flows are a separate failure mode from Details.
- Deepseek is correct here.
- Any update/modify endpoint that re-reads the old row for audit, validation, or diffing needs an existence check before using the old data.
- That is a controller/repository responsibility, not something the details-page fix will solve.
-
Print flows are also separate.
- If the print endpoint depends on the current record state, it needs its own missing-record guard before generating output.
- If it prints from already-loaded client state, it still needs a backend-side recheck before generating a PDF or email attachment if stale deletes matter.
My amended lean plan:
- Phase 1: make all shared document detail endpoints return
404for missing ids. - Phase 2: add one shared web-client helper that turns
404GETs intonullcleanly. - Phase 3: add a shared page-load guard pattern for details/edit/print pages so they redirect once when the model is missing.
- Phase 4: patch modify/print endpoints that dereference old data to check existence first.
Best alternative to the original proposal:
- Do not try to fix this with a global exception handler.
- Do not patch each page with ad hoc try/catch.
- Do use one client helper plus one page pattern plus controller existence checks.
Net: the fix still stays lean, but Deepseek correctly forces one amendment — you need a shared 404-aware GET helper, because GetFromJsonAsync will not quietly return null once the API is corrected.
Claude code’s assessment and suggested fix
What actually crashes vs. what’s bad UX
There are two distinct failure modes, not one:
┌─────────────────────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────┬─────────────────────┐ │ Mode │ What happens │ Is it a crash? │ ├─────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────┼─────────────────────┤ │ Server-side NRE │ Modify/Print/Delete endpoints do repo.Details(id).Result → null → data.SomeProperty → NRE │ Yes, hard crash │ ├─────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────┼─────────────────────┤ │ Client-side broken │ GetFromJsonAsync + ?? new T() → empty object with Id=0, all strings null → page tries │ Depends on page, │ │ page │ _Voucher.YearVoucherNo or .ToString(“dd-MMM-yy”) on null │ often yes │ └─────────────────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────┴─────────────────────┘
Where the document’s plan over-engineers
The plan’s Phase 1 — changing Ok(null) → NotFound() across ~100 controllers — is ceremony without benefit. Here’s why:
- It’s an internal API. The semantic correctness of 404 vs 200 null buys you nothing — no external consumer, no API versioning, no HATEOAS client.
- It creates a cascade: 404 → GetFromJsonAsync throws → need GetOrNullAsync in ~100 services → need try-catch or helper in every service. All to end up with… the same null you already had from Ok(null).
- The ?? new T() pattern already works as a client-side NRE shield. It’s ugly but functional.
The plan is architecturally correct for a public REST API. For an internal Blazor monolith, it’s the wrong tradeoff.
What I’d do instead
Two changes. Skip the HTTP contract change entirely.
Fix 1: Server-side null guards (~65 endpoints)
Every modify/print/delete endpoint that dereferences repo.Details(id).Result:
var data = _IVoucherRepository.VoucherDetails(id, dbname).Result; if (data == null) return NotFound(“This record was deleted by another user.”);
This is what VoucherModify at line 1323 needs — currently _OldValues becomes null, then DetectChanges(null, …) NREs. Same pattern in ~65 other modify/print endpoints.
Fix 2: Client-side sentinel check (~100 pages)
In OnInitializedAsync, after loading:
_Voucher = await _IVoucherService.VoucherDetails(id, dbname); if (_Voucher.Id == 0) { /* snackbar + redirect to index */ return; }
Id == 0 is the sentinel — the ?? new T() fallback always produces Id=0 because auto-increment keys start at 1. Cheaper than changing the HTTP contract and plumbing 404 through every service.
What I’d skip
- Skip changing Ok(null) → NotFound() — ceremony, no crash prevented
- Skip GetOrNullAsync helper — unnecessary when ?? new T() + sentinel check already works
- Skip concurrency tokens (rowversion) — separate feature, ships zero crash-fix value
- Skip global exception handler — hides real NREs
Summary
The document’s plan is ~300 files touched. My alternative is ~165 files touched, all 3-line mechanical changes — find-and-replace per document family. Same crashes fixed, half the churn, no new abstractions.
The core insight: Ok(null) is not the bug. Dereferencing null without checking is the bug. Fix the dereferences, don’t re-plumb the API contract.
Chatgpt’s assessment
I read through the document carefully.
My assessment is that this is a good engineering design discussion because it doesn’t stop at “how do we stop the exception?” It explores multiple architectural approaches, their trade-offs, and even includes critiques from different perspectives. That is exactly how this kind of cross-cutting problem should be evaluated.
That said, I think each proposal optimizes for a different goal.
The original proposal
Technically sound.
Returning 404 NotFound() for a missing resource is the correct HTTP contract. If someone requests /voucher/1000 and voucher 1000 no longer exists, the resource is not found.
From an API design perspective, I completely agree with this.
The problem is cost.
Changing from Ok(null) to NotFound() ripples through every consumer because GetFromJsonAsync<T>() changes behaviour. What was previously
nullbecomes
HttpRequestExceptionSo one “correct” change forces changes in many service methods.
Architecturally elegant, but expensive.
DeepSeek’s review
This is the strongest technical review in the document.
It correctly points out something many people overlook:
Changing the server contract changes client behaviour.
That observation is absolutely correct.
It also correctly separates:
- Details pages
- Modify pages
- Print pages
Those are genuinely different execution paths.
I agree with almost everything DeepSeek says.
The rejoinder
I think this is the most balanced section.
It accepts DeepSeek’s criticisms without abandoning the architectural goal.
The staged rollout is sensible:
- API
- helper
- page guard
- modify/print fixes
If you choose the REST-correct approach, this is probably the cleanest migration path.
Claude’s alternative
This is where I disagree.
Claude argues:
“Ok(null) is not the bug.”
I think that’s only partially true.
The immediate crash is indeed caused by dereferencing a null object.
But returning
200 OKfor
resource does not existis still semantically wrong.
It works inside a monolith, but it makes the API lie.
If five years from now you expose these APIs to mobile apps or external integrations, you’ll probably end up changing them anyway.
So I would not dismiss the HTTP contract as “ceremony.”
Claude’s proposed sentinel approach:
if (_Voucher.Id == 0)works.
But it has drawbacks.
Now your application depends on a convention:
“Id 0 means object not found.”
That convention must be remembered forever.
It’s invisible.
A new developer won’t know it.
A nullable return or a 404 is self-documenting.
What I think is actually the best solution
I don’t think either extreme is ideal.
I would use a hybrid approach.
Phase 1 (Immediate)
Fix every place that crashes.
That means:
- Modify
- Delete
- Audit
- Any code dereferencing repository results
Those should never assume the row exists.
This fixes production bugs immediately.
Phase 2
Introduce one helper for GETs.
Something like
GetOrNullAsync<T>()or
TryGetAsync<T>()Now all clients have a consistent way to say
“404 means missing.”
Phase 3
Gradually convert controllers to
return NotFound();There is no need to convert 100 controllers in one commit.
As modules are touched for other work, update them.
Phase 4
Use one reusable UI pattern.
Something like
if (model == null)
{
Snackbar.Add("This document was deleted by another user.");
Navigation.NavigateTo("/voucher");
return;
}Don’t duplicate lots of page-specific logic.
One thing I think nobody mentioned
The document focuses entirely on handling the error after it occurs.
I’d also consider improving the user experience.
For example:
Voucher deleted.
This document is no longer available because another user deleted it.
[Return to Voucher List]instead of showing a generic error or silently redirecting.
For business users, that explanation is much clearer.
Overall assessment
If I were reviewing this as a software architect, I’d rate it:
- Problem analysis: 10/10
- Coverage of scenarios: 9.5/10
- API design discussion: 9/10
- Implementation practicality: 8.5/10
My only criticism is that the discussion becomes somewhat polarized between “REST purity” and “minimal code churn.” In practice, you don’t have to choose one or the other. A phased migration gives you the architectural correctness of 404 NotFound() without forcing a massive, risky refactor all at once.
For an ERP system with many document types (Voucher, Invoice, Bill, GRN, Orders, Indents, etc.), I would favour the phased hybrid approach. It minimizes production risk, keeps the codebase moving toward better API semantics, and avoids a disruptive “big bang” change.
Yes. If you want the user experience to be:
Voucher deleted. This document is no longer available because another user deleted it. [Return to Voucher List]
then there is really no way around making changes in multiple places. However, I don’t think they’re as daunting as they first appear.
The key question is: where is the minimum number of places you need to touch?
I’d classify the work like this:
| Layer | Change | Can it be centralised? |
|---|---|---|
| Repository/API | Return missing record cleanly | Yes |
| Web Service | Convert missing record to null |
Yes |
| Razor Page | Check for null and redirect |
No (but mechanical) |
| Modify Controller | Guard before using old values | No |
| Print Controller | Guard before generating output | No |
The Razor Pages are the unavoidable part. Every page that loads an entity has different variables, different navigation targets, and different initialisation logic. There isn’t a single place in Blazor Server where you can intercept “model not found” and automatically redirect every page.
For example, every page ends up looking something like:
_Voucher = await _VoucherService.VoucherDetails(id, dbname);
if (_Voucher == null)
{
Snackbar.Add("This voucher was deleted by another user.", Severity.Warning);
NavManager.NavigateTo("/voucher");
return;
}That’s only about 4–5 lines, but they have to exist somewhere in the page’s loading logic.
The good news
This is exactly the kind of change AI tools are excellent at making.
Every Details page follows almost the same pattern.
Every Modify page follows almost the same pattern.
Every Print page follows almost the same pattern.
So although you may touch 150–200 files, you’re not writing 200 different solutions. You’re applying the same small edit repeatedly.
If I were planning this project
I wouldn’t treat it as “fixing vouchers.”
I’d define a new application-wide standard:
Any page that loads a document by ID must gracefully handle the document no longer existing.
Then I’d work module by module:
- Voucher
- Invoice
- Bill
- GRN
- Orders
- Indents
- …
Each module would be completed and tested before moving to the next.
One suggestion that could reduce future maintenance
Since you have many document pages with similar behaviour, consider creating a small helper method or base class. For example:
protected bool HandleMissingDocument<T>(T? model, string returnUrl)
{
if (model != null)
return true;
Snackbar.Add(
"This document was deleted by another user.",
Severity.Warning);
NavManager.NavigateTo(returnUrl);
return false;
}Then each page becomes:
_Voucher = await _VoucherService.VoucherDetails(id, dbname);
if (!HandleMissingDocument(_Voucher, "/voucher"))
return;You still touch every page once, but the actual behaviour is centralised. If you later decide to change the message, log telemetry, or display a richer dialog, you do it in one place rather than editing every page again.
So my conclusion is:
- Yes, every page that loads a document will need a small change.
- No, I don’t see a practical way to eliminate those per-page edits entirely.
- I would invest a little extra effort in creating a shared helper or base class, because that turns this one-time refactoring into a permanent improvement to your application’s consistency.
Claude.ai Sonnet 5
Read through the whole thread — the debate between your assistants, Deepseek’s review, the rejoinder, Claude Code’s counter-plan, and ChatGPT’s hybrid. Here’s my honest read.
The debate everyone had, and the one nobody quite landed on
Four AIs argued over roughly this axis: “make the API return 404” (architecturally pure, but cascades into ~300 files) vs. “just fix the null dereferences” (pragmatic, but Claude’s own Id == 0 sentinel is a smell — ChatGPT called this correctly).
But there’s a cheaper fix underneath both positions that got missed: your API almost certainly already returns real null to the client.
Ok(null) serializes to a JSON body of literal null. GetFromJsonAsync<T>() deserializing null for a reference type returns actual null — not an exception, not a 404, just null. The only reason your Razor pages don’t see that null today is the ?? new T() fallback in the service layer, which is manufacturing the fake empty object with Id = 0 that’s causing the downstream garbage. That fallback is the bug, not Ok(null) itself.
So:
- Claude Code’s
Id == 0sentinel works, but it’s solving a problem your own service layer created by masking a null that was already there. It’s also fragile — a new dev won’t knowId == 0means “deleted,” and it silently breaks if any code path legitimately constructs an unsavedId = 0object for a different reason. - Deepseek/Rejoinder’s
NotFound()+GetOrNullAsynccascade solves a problem you don’t have. You don’t need to change the HTTP status to getnullon the client — you already get it, you’re just discarding it. - ChatGPT’s critique of the sentinel is right, but its proposed hybrid still keeps the 404 migration as “Phase 2,” which is unnecessary work for zero additional crash coverage.
Net: this is a case where the “pragmatic” side won on outcome, but everyone (including Claude Code) still added one layer of unneeded machinery. The actual minimum fix is smaller than any of the four proposals.
My assessment, plainly
- Problem analysis in the doc: excellent, no notes.
- The 404-vs-200 debate: mostly a wash — real-world impact is near zero either way for an internal Blazor monolith, but it burned the most words.
- The one thing worth fixing that all four semi-missed: stop masking null at the service boundary. That alone removes the need for a sentinel and removes the need for a contract change.
Exact action plan (sequenced for your validate-one-thing-at-a-time style)
Phase 0 — Audit (do this first, don’t touch code yet)
- Grep the service layer for the null-masking pattern:
?? new Voucher(),?? new T(), or equivalent per-module fallbacks. - Grep controllers for
.Details(id).Result(orawait ...Details(id)) used inside Modify/Delete/Print actions, where the result is dereferenced without a null check — e.g.VoucherController.cs:1314(audit trail) andVoucherController.cs:1674(print). - Get exact counts per module (voucher/invoice/bill/GRN/orders/indents) before deciding module order.
Phase 1 — Fix the actual crashes (server-side, highest priority, module by module) In every Modify/Print/Delete controller that re-reads the old row and dereferences it:
var data = _IVoucherRepository.VoucherDetails(id, dbname).Result;
if (data == null)
return NotFound("This record was deleted by another user.");Do voucher first (it’s the one already logging the WRN), validate in isolation, then repeat per module. This is the only part that’s an actual NRE today — everything else is UX.
Phase 2 — Remove the null-masking fallback (service layer, mechanical, one module at a time)
Delete the ?? new T() in each GET service method so a missing record comes through as real null instead of a fake object with Id = 0. No GetOrNullAsync helper needed — GetFromJsonAsync already returns null correctly once you stop overwriting it.
Phase 3 — Shared UI guard on Details/Modify/Print pages (module by module) One helper, one call site per page:
protected bool HandleMissingDocument<T>(T? model, string returnUrl)
{
if (model != null) return true;
Snackbar.Add("This document was deleted by another user.", Severity.Warning);
NavManager.NavigateTo(returnUrl);
return false;
}_Voucher = await _IVoucherService.VoucherDetails(id, dbname);
if (!HandleMissingDocument(_Voucher, "/voucher")) return;Explicitly skip:
- Changing
Ok(null)→NotFound()anywhere. No benefit; it’s an internal API. GetOrNullAsync/try-catch wrapper for GET calls — unnecessary once Phase 2 is done.- Concurrency tokens/rowversion — legitimate future improvement, zero relevance to this bug, defer entirely.
- Global exception handler — would mask real NREs elsewhere.
Rollout order: Voucher → Invoice → Bill → GRN → Orders → Indents, each module fully done (Phase 1+2+3) and tested with one deleted-record scenario before moving to the next — matches how you’ve sequenced the other infra changes.
This lands you at roughly 3 layers touched per module instead of the ~5-layer, HTTP-contract-changing version, with the same crash coverage and a cleaner mental model: null means null, all the way through.
Final implementation plan from Antigravity
Final Minimal & Optimal Implementation Plan: Handling Deleted Records
Synthesis of Full AI Multi-Model Evaluation
Across all discussions (Codex, OpenCode, Claude Code, ChatGPT, DeepSeek V4, and Claude 3.5/3.7 Sonnet), the root cause and the absolute minimal fix were identified:
- Root Cause 1 (Server NREs): In API controllers (Modify, Print, Email, Delete actions), code calls
_repository.Details(id).Resultand dereferences properties without checking if the result isnull. When a record was deleted,null.VoucherTypeCodeorDetectChanges(null, ...)throws a server-sideNullReferenceException. - Root Cause 2 (Client Null-Masking): In
ErpCrystal_MFG.Webservices, methods end with?? new T()(e.g.?? new Voucher()). When the API returns200 OKwith JSON bodynull,GetFromJsonAsyncreturnsnull, but?? new Voucher()manufactures a fake object withId = 0. Blazor pages receive this fake object and crash when attempting to slice strings (YearVoucherNo[..3]) or format dates.
The Minimal 3-Point Implementation Plan
This solution avoids unnecessary HTTP contract refactoring, custom middleware, or framework hacks. It requires zero changes to API status contracts (Ok(null) stays as-is).
sequenceDiagram
participant UI as Blazor Page
participant Service as Web Service
participant API as Api Controller
participant DB as SQL Database
UI->>Service: VoucherDetails(id)
Service->>API: GET api/voucher/voucherdetails/{id}
API->>DB: QuerySingleOrDefault (deleted ID)
DB-->>API: null
API-->>Service: 200 OK (JSON null)
Note over Service: Remove '?? new Voucher()' fallback
Service-->>UI: returns actual null
Note over UI: HandleMissingDocument(_Voucher, "/voucherindex")
UI-->>UI: Snackbar Warning + Redirect to IndexStep 1: Server-Side Null Guards (API Modify/Print/Delete Actions)
In API controllers (VoucherController.cs, JournalVoucherController.cs, InvoiceController.cs, etc.), guard against null before dereferencing old data:
// Example in VoucherModify / VoucherPrint / VoucherValidateEmail:
var oldValues = _IVoucherRepository.VoucherDetails(id, dbname).Result;
if (oldValues == null)
{
return NotFound("This record was deleted by another user.");
}Step 2: Remove Service Null-Masking (Web Services Layer)
In ErpCrystal_MFG.Web/Services (e.g. VoucherService.cs), remove ?? new T() so missing records return actual null:
// BEFORE:
return await _HttpClient.GetFromJsonAsync<Voucher>($"api/voucher/voucherdetails/{id},{dbname}") ?? new Voucher();
// AFTER:
return await _HttpClient.GetFromJsonAsync<Voucher>($"api/voucher/voucherdetails/{id},{dbname}");Step 3: Shared UI Missing Document Guard (Blazor Pages)
Create a single shared helper method (e.g., in a helper or base class):
public static bool HandleMissingDocument<T>(T? model, ISnackbar snackbar, NavigationManager navManager, string returnUrl) where T : class
{
if (model != null) return true;
snackbar.Add("This document was deleted by another user.", Severity.Warning);
navManager.NavigateTo(returnUrl);
return false;
}In Blazor Details.razor, Modify.razor, and Print.razor pages (OnInitializedAsync):
_Voucher = await _IVoucherService.VoucherDetails(Convert.ToInt32(id), _PostLogin.dbname);
if (!DocumentHelper.HandleMissingDocument(_Voucher, _ISnackbar, NavManager, "/voucherindex"))
return;Phased Module Rollout
To keep execution safe, surgical, and verifiable:
- Module 1:
Voucher(VoucherController.cs,VoucherService.cs,VoucherDetails.razor,VoucherModify.razor) - Module 2:
JournalVoucher - Module 3:
Invoice&Dncn(Debit/Credit Note) - Module 4:
SalesOrder,Bill,Grn
Verification Plan
Automated Verification
dotnet build ErpCrystal_MFG.slnVerify zero build errors across solution.
Manual Verification Flow
- Concurrent Delete Test:
- Open Voucher #1000 in Tab 1 (
/voucherdetails/1000or/vouchermodify/1000). - Delete Voucher #1000 in Tab 2.
- Click Refresh or Save in Tab 1.
- Result: Yellow Snackbar toast (“This document was deleted by another user.”) appears and browser cleanly redirects to
/voucherindexwithout server NREs or client UI crashes.
- Open Voucher #1000 in Tab 1 (
Final comments from Claude.ai
Yes — this is good and matches where I landed. It correctly threw out the whole 404/GetOrNullAsync cascade, kept Ok(null) untouched, and put the fix exactly where the real bugs live: null-derefs in Modify/Print/Delete, and the client-side masking in the service layer. Nothing in it is wrong. A few refinements before you start Module 1:
1. Let the compiler find your 150 files for you — don’t grep manually.
Once you delete ?? new Voucher(), if nullable reference types are enabled (or you turn them on for the touched projects), the method signature should become Task<Voucher?>. Every caller that doesn’t null-check will throw a CS8602 possibly-null-dereference warning at build time. That turns “manually audit every Details/Modify/Print page across 6+ modules” into “build, read the warning list, fix each one, repeat.” It’s the single biggest time-saver available here and it’s a free safety net against missed call sites — do this per-module as you go, not as a giant one-shot.
2. Audit non-page callers of the same service methods, not just OnInitializedAsync.
The verification plan itself tests clicking “Save” in Tab 1 after a delete — that means some Save/Submit handlers re-fetch via the same VoucherDetails service call mid-edit (for a diff, or revalidation) outside the page-load path. Those call sites need the same HandleMissingDocument guard, not just the initial page load. Step 1’s compiler-warning sweep above will catch these automatically if nullable is on — worth confirming before you consider a module “done.”
3. Modal/dialog-based Details views need a variant of the helper.
If any Details/Modify flows open in a MudBlazor dialog rather than a full page navigation (common in grid “view” actions), NavManager.NavigateTo(returnUrl) won’t make sense mid-dialog. Check for this per module — you may need a HandleMissingDocument overload that closes the dialog (MudDialogInstance.Close()) instead of navigating.
4. Indents is missing from the rollout list.
Your original scenario named voucher, invoice, bill, GRN, orders, and indents. The plan’s Module 4 covers SalesOrder/Bill/Grn but drops Indents — add it as Module 5 (or fold in) so it doesn’t get silently skipped.
5. Confirm client-side print generation, not just the API print endpoint. Step 1 guards the API’s print action. If any Blazor page independently loads the record via the web service before calling a report/PDF generator client-side (rather than everything routing through the API), that load site needs the same null guard before it hits the report library.
None of these are blockers — they’re checklist items to close out per module as you validate. Ship Module 1 (Voucher) exactly as written, confirm the compiler-warning approach surfaces the Save-handler and any modal call sites cleanly, then repeat for the rest. That first module will tell you whether refinements 2–3 actually apply to your codebase or were already covered.
Final Prompt to Antigravity
Please see a few refinements suggested by Claude.ai. Could we take these into a/c?
Also batchwise plan will be as follows
- Voucher, JV (Finance Module)
- Sales orders, Invoices (Sales Module)
- Indents, GRNs, Purchase bills, Debit/credit Notes (Supply chain)
- IIRS, Production Plan, Jobwork (Manufacturing)
- Employee master (Human Assets)
- Item Master, Party Master, Price Master (System Tools) Many of these features include a document level and also a line level. We need to do in both levels. Please revise the plan accordingly. Any questions?
Final Plan Batchwise
Minimal & Optimal Implementation Plan: Handling Deleted Records
Executive Summary
This plan provides a clean, surgical, and minimal fix for missing or concurrently deleted records across all application modules.
- Standard User Message:
"This document is not available or may have been deleted!" - Scope: Applied to both Document Level (Header/Main) and Line Level (Items/Sub-lines).
- Zero API Contract Churn: Preserves
Ok(null)without forcing a 404 refactor across controllers. - Client Null-Masking Fix: Removes
?? new T()fallbacks in Web services so missing records returnnullinstead of fake objects withId = 0. - Server Safety: Adds null guards on API endpoints before accessing deleted record properties during audit trail, print, or update operations.
Technical Solution (Document & Line Level)
1. Server-Side Null Guards (ErpCrystal_MFG.Api)
In API controllers (Modify, Print, Email, Delete actions for both Main and Line levels), guard against null before dereferencing old data:
// Document & Line Level API Action Guard:
var oldValues = _IVoucherRepository.VoucherDetails(id, dbname).Result;
if (oldValues == null)
{
return NotFound("This document is not available or may have been deleted!");
}2. Remove Service Null-Masking (ErpCrystal_MFG.Web/Services)
In Web service GET methods for both Main and Line items, remove ?? new T() so missing records return actual null:
// BEFORE:
return await _HttpClient.GetFromJsonAsync<Voucher>($"api/voucher/voucherdetails/{id},{dbname}") ?? new Voucher();
// AFTER:
return await _HttpClient.GetFromJsonAsync<Voucher>($"api/voucher/voucherdetails/{id},{dbname}");3. Shared UI Missing Document Guard (ErpCrystal_MFG.Web)
Use a shared helper method in Blazor pages (OnInitializedAsync and submit handlers):
public static bool HandleMissingDocument<T>(T? model, ISnackbar snackbar, NavigationManager navManager, string returnUrl) where T : class
{
if (model != null) return true;
snackbar.Add("This document is not available or may have been deleted!", Severity.Warning);
navManager.NavigateTo(returnUrl);
return false;
}In Blazor Details.razor, Modify.razor, LineModify.razor, and Print.razor pages:
_Voucher = await _IVoucherService.VoucherDetails(Convert.ToInt32(id), _PostLogin.dbname);
if (!DocumentHelper.HandleMissingDocument(_Voucher, _ISnackbar, NavManager, "/voucherindex"))
return;6-Batch Module Rollout Plan
Implementation will proceed sequentially by business module, covering both Document Level and Line Level pages/endpoints:
| Batch | Business Module | Covered Documents & Line Items |
|---|---|---|
| Batch 1 | Finance Module | Voucher (Main & Lines Voucher1), Journal Voucher (Main & Lines JournalVoucher1) |
| Batch 2 | Sales Module | Sales Orders (Main & Lines SalesOrder1), Invoices (Main & Lines Invoice1) |
| Batch 3 | Supply Chain | Indents, GRNs, Purchase Bills, Debit / Credit Notes (Dncn) |
| Batch 4 | Manufacturing | IIRS, Production Plan, Jobwork |
| Batch 5 | Human Assets | Employee Master |
| Batch 6 | System Tools | Item Master, Party Master, Price Master |
Verification Plan
Automated Build Verification
dotnet build ErpCrystal_MFG.slnManual Functional Testing (per batch)
- Document Level Concurrent Delete:
- Open Main Document (e.g. Voucher #1000) in Tab 1. Delete in Tab 2.
- Click Save/Modify/Print in Tab 1.
- Expected Result: Snackbar warning
"This document is not available or may have been deleted!"and clean redirection to Index page.
- Line Level Concurrent Delete:
- Open Line Item Modify (e.g. Voucher1 line #5) in Tab 1. Delete parent/line in Tab 2.
- Click Save in Tab 1.
- Expected Result: Same warning message
"This document is not available or may have been deleted!"and clean redirection to Index page.