Government financial platforms tend to grow their reporting layer one page at a time. A new report is needed, a new page is added, filters are wired up, and it ships. The pattern repeats over years and across teams. By the time anyone looks at it holistically, there are 30 isolated pages, each with its own filter setup, each requiring the user to re-enter the same contractor, contract, and period-of-performance selections they just entered on the page they came from. Nothing is bookmarkable. Shared links do not work. The browser back button is unreliable.
This is not the result of poor decisions. It is the predictable outcome of building one report at a time under program deadlines, without the upfront investment in a shared filter infrastructure. Each individual decision was reasonable. The cumulative result is a reporting experience that treats every session as context-free.
Consolidating this kind of fragmented reporting surface into a unified workspace is a real improvement worth making. It is also work that is more involved than it looks from the outside — and the piece that most often gets underestimated is not the new frontend. It is the audit work that has to come before it.
The session state dependency problem
In a legacy ASP.NET WebForms application, session state is the glue that holds multi-page workflows together. A user selects a contractor on the dashboard. That selection is written to a session key. The next page reads it. The page after that reads it too. Maybe a fourth page writes an updated version. Maybe the stored procedure called on page three takes a department ID from a different session key, which was set somewhere else entirely.
This dependency graph is invisible unless you map it. There is no contract, no declared API, no type system enforcing it. A key can be written anywhere and read anywhere. The only way to know what depends on what is to search the codebase and trace every occurrence.
The reason this matters for a modernization is straightforward: if you migrate a page without knowing it was a producer of session state that another page consumes, you break the consumer. If you migrate a page without knowing it depends on session state set two pages back in a flow you are not migrating yet, you break the page you just shipped. These are not edge cases. In a large application with years of session-based state threading through it, these dependencies are pervasive.
The session state audit is not glamorous pre-work. It is the work that makes the migration safe.
Stored procedure parameters sourced from session
The session state dependency problem extends beyond page navigation. In government financial applications, stored procedures often accept parameters — department ID, user ID, contractor ID — that were previously sourced from session state. The proc signature does not change in the new system. But the source of those values does.
In the new application, those values come from JWT claims and service-layer lookups against the user's identity record. The identity token maps to an internal user record, which maps to a department and a role. The contractor scope is resolved from role assignments, not from a session key that was set when the user selected a contractor from a dropdown.
This is a cleaner design, but it requires you to know which parameters need this treatment. The only way to know is to review every stored procedure the migrated pages call and flag any parameter that was previously session-sourced. That review happens during the pre-work phase, before any new code is written — because discovering a session-sourced parameter mid-implementation requires reworking the service layer under delivery pressure, which is the worst time to do architectural design.
URL state as the correct replacement
Session state in a web application is doing one of two things: holding short-lived transient data the server needs between requests, or passing navigational context between pages. The second use case — the dominant one in reporting-heavy government applications — has a correct architectural replacement: URL state.
Every filter selection encoded in URLSearchParams produces a unique, bookmarkable URL. The user selects a contractor, a contract, one or more periods of performance, and a time bucketing option, then hits Apply. The URL encodes the full filter context. That URL can be shared across teams, saved as a browser bookmark, linked from a ticket or an email, or copied into a status report. When the recipient opens it, they see exactly what the sender saw.
This is not a UX improvement bolted onto the migration. It is the correct structural replacement for what session state was trying to do in the navigation layer. The filter state belongs in the URL, not in a server-side dictionary that evaporates when the session expires or when someone opens the same report in two browser tabs.
The Apply button matters. The correct design cascades each dropdown — selecting a contractor reloads the contract dropdown, selecting a contract reloads the task order and POP dropdowns — but the URL and the data load only update on Apply. Triggering a data load on every intermediate selection creates race conditions and over-fetching. Apply is the point at which the user declares their intent, and that is the point at which the URL should update and the API call should fire.
The global filter bar as a unified context layer
A persistent global filter bar across all report tabs is the UI expression of the architectural decision to share filter context. It sounds obvious, but it requires deliberate design: the filter bar has to be a shared component that survives tab switches, that reads its initial state from the URL on mount, and that writes its state back to the URL on Apply.
The cascade order matters and is not arbitrary for government financial data. The natural hierarchy is Contractor → Contract → Task Order → Period of Performance → Date Range → Time Period. Each selection narrows the valid options for the next. The filter bar encodes the data hierarchy of the application, and getting that hierarchy right is a domain design question as much as a UX one.
Tab-specific extra filters — filters that only apply to one report tab — should be separate from the global filter bar and collapsed by default. The global bar stays visible and persistent across all tabs. Tab-local filters are scoped to the current view and accessible but not prominent. This keeps the shared context visible without polluting it with per-tab specifics.
Why the stored procedures should not change
Stored procedures in government financial systems carry production-validated calculations that may have been tuned over years of budget cycles and audit reviews. The burn rate logic, the variance calculations, the time-bucketing for period aggregation — these have been verified against real financial data in a regulated environment. They work.
The instinct during a migration is to clean them up. Replace the scalar UDF that prevents query parallelism with a modern equivalent. Simplify the SELECT branches for time bucketing. Move the department-scoping logic out of the proc and into the service layer where it belongs architecturally.
These are legitimate improvements. They are not Phase 1 work.
Rewriting stored procedures during a migration introduces regression risk with no user-visible benefit. If the new proc produces a different result for any input combination — even a boundary case that appears infrequently — you will not know it until a user notices a discrepancy in production financial data. In a government acquisition context, that is a serious problem.
The correct approach is to call the existing proc via Dapper with the same parameter types and values it has always received. Move the session-sourced parameters to JWT-based resolution in the service layer — that is a service layer change, not a proc change. Compute any additional derived values in C# from the raw proc output. The proc signature is unchanged. The proc code is unchanged. The only thing that changes is where the input values come from.
The operational workspace to reporting workspace seam
A government financial platform typically has two kinds of users: people who manage the data — entering periods of performance, submitting cost reports, updating contract details — and people who analyze it — running financial dashboards, variance reports, billing reviews. In a legacy system these workflows are often split across different pages with no coherent connection between them.
The seam that makes the modernized platform coherent for both user types is the deep link. From any period-of-performance row in the operational workspace, there should be a pre-populated link that navigates directly to the reporting workspace with all filters pre-filled: the contractor, the contract, the task orders, the POP, and the tab.
The user clicks that link from the POP they were just editing and lands on the Financial tab with the correct data already loaded. No filter re-entry. No navigation from scratch. The two areas of the platform are connected by a URL that encodes the full context of where the user came from.
This is not a complex feature to implement. It is a link. What makes it possible is the URL-driven filter state design — the reporting workspace has to read its filter context from the URL on mount, not from session state or from component state accumulated through user interaction. The operational-to-reporting seam is essentially free once the URL architecture is right.
Protabyte works with government agencies, federally-affiliated organizations, and regulated-environment technology teams on platform modernization — including financial reporting systems, legacy WebForms migration, and the architectural work required to replace session-based state with maintainable, URL-driven navigation. If your reporting surface has fragmented over time and the filter re-entry burden has become a real operational cost, we are available for a direct conversation.