DEV Community

Ivan Rossouw
Ivan Rossouw

Posted on AI-assisted

A Green Build Is Not a Route Test

A Blazor Hybrid application can compile successfully and still fail as soon as its router starts.

The uncomfortable part is that nothing needs to be wrong with the individual components. The failure can live in the composition of otherwise valid Razor pages: two scanned assemblies declare the same route, and the conflict is only discovered at runtime.

That makes route ownership an architectural contract, not just an @page detail.

The invisible route set

Multi-head applications often share UI through a Razor class library. A web host, a progressive web app, and a .NET MAUI host may each scan two places:

  1. the host's own assembly; and
  2. a shared UI assembly.

This is useful. Shared pages can be implemented once while each host keeps pages that depend on its own authentication model, platform services, or startup behaviour.

It also creates a set that the compiler does not reason about: all routes visible to one router.

Imagine the shared assembly contains:

@page "/settings"
Enter fullscreen mode Exit fullscreen mode

Later, a host-specific page independently declares the same path. Both components are valid. Both assemblies compile. The duplicate becomes visible only when that host's router scans the combined set.

Why a green build is insufficient

Compilation proves that each project is structurally valid. It does not prove that independently compiled route tables are unique when combined.

This is a general modularity problem. A module can be correct in isolation while the application graph is invalid after composition. Dependency-injection cycles, duplicate HTTP endpoints, conflicting configuration keys, and route collisions all share this shape.

For Blazor, the timing makes the failure especially awkward. The collision can wait until the router initialises on first load. A pipeline may restore, compile, and test successfully, while the person opening the application becomes the integration test.

Why the MAUI host changes the decision

Web applications can cheaply receive post-deploy smoke coverage: request a page and assert that it renders instead of returning an error.

A MAUI build is different. Producing an Android or iOS binary does not prove that a BlazorWebView can start, compose its scanned assemblies, and navigate. A real-device or emulator startup test is valuable, but it adds workload installation, boot time, device management, and another source of CI instability.

In the committed change I reviewed, the web-facing heads had page smoke coverage while the MAUI workflow had no equivalent startup check. That made a fast structural guard a sensible intermediate layer.

Build a focused collision guard

The guard does not need to reproduce the whole Razor runtime. It needs to answer one architectural question:

Within the assemblies scanned by each host, is any route declared more than once?

A practical implementation can:

  • enumerate .razor source files for a host and its shared UI project;
  • ignore generated bin and obj trees;
  • remove Razor and HTML comments;
  • recognise only complete @page directive lines;
  • normalise paths with an explicit case policy; and
  • group the resulting declarations by route.

Run the same assertion for every head. A collision report should name the generalized host and the two source locations so a developer can resolve ownership quickly.

This test is intentionally narrow. It does not need a browser, emulator, dependency container, or application server. Its value is that it turns a late composition failure into an early, deterministic signal.

Make the guard prove itself

Source guards have a dangerous failure mode: they can pass because the parser found nothing.

That is why a negative assertion such as “there are no duplicates” is incomplete. Add an anti-vacuous test that requires the parser to find a few routes that must exist. If a future Razor syntax change, path change, or parser bug empties the result, the test fails instead of giving false confidence.

Test false positives too. Comments often contain route examples:

@* Do not duplicate @page "/settings" in this host. *@
Enter fullscreen mode Exit fullscreen mode

A naïve text search counts that as a declaration. A focused parser should ignore it, and a test should pin that behaviour. Excluding generated copies matters for the same reason; otherwise every route can appear to collide with itself.

The trade-off

A source-level guard is fast and portable, but it is coupled to project layout and Razor syntax. It does not exercise the real router, dependency injection, native lifecycle, or device packaging.

A device startup test provides higher-fidelity evidence, but it is slower and more operationally expensive. The two tests answer different questions:

  • source guard: is the declared route set structurally unique for every host?
  • device smoke test: can the packaged application actually start and navigate in its platform environment?

Use the source guard for immediate feedback, then add at least one device-startup path when the release risk justifies the cost.

A practical checklist

Before adding or moving a shared Razor page:

  1. Write down every assembly each host scans.
  2. Decide whether the route is shared or host-owned.
  3. Assert uniqueness across each host's complete scanned set.
  4. Prove the guard finds known routes.
  5. Prove comments and generated output do not create false routes.
  6. Keep a device startup check on the roadmap, because structural tests are not runtime proof.

Shared UI is valuable precisely because it removes duplication. The price is making the composition rules explicit. A green build tells you the parts compile. A route test tells you whether those parts can safely meet.

Top comments (0)