An ASP.NET Core application can return HTTP 200 and still execute the wrong controller. A broad attribute route such as {culture}/{brand} can interpret /en/About as a brand request even when the intended destination is AboutController.Index. This guide turns that quiet routing defect into a small, repeatable test, then shows how constraints, endpoint logs, response markers, and a route matrix produce a durable fix.

Direct answer: why an unconstrained attribute route captures other pages

An unconstrained {culture}/{brand} template accepts almost any two path segments, so /en/About can become a valid candidate for the brand action. Restrict both values—or add a literal prefix—then verify ordinary controller pages and legitimate brand URLs together instead of relying on registration order.

The useful mental model is not simply “attribute routes always run first.” ASP.NET Core endpoint routing builds a candidate set, matches route templates, applies constraints and matcher policies, and then selects a valid endpoint using precedence and metadata. Literal segments are more specific than parameter segments, constrained parameters are more specific than unconstrained parameters, and catch-all parameters are the broadest. A robust design therefore gives each endpoint an explicit URL territory rather than depending on an incidental ordering outcome.

Assume HomeController declares [HttpGet("{culture}/{brand}")], while conventional routing maps {culture}/{controller}/{action=Index}. The path /en/About has two plausible interpretations. The attribute action sees culture=en and brand=About; the conventional route sees culture=en and controller=About. If the brand endpoint wins, the browser receives a successful response from the wrong action. Availability monitoring that checks only status codes will not detect the semantic failure.

This distinction matters because the Razor view, cache, and build output may all be correct. Rebuilding repeatedly does not change the endpoint selected for an overlapping URL. The defect is ownership ambiguity in the route table. The correction must narrow or redesign that ownership, and the acceptance test must prove that both sides of the boundary remain reachable.

Identify the endpoint before changing the view

Start with evidence about execution, not assumptions about rendering. Put a unique, harmless HTML comment such as `` at the beginning of the view you expect to run. Rebuild the local test application and fetch the response body. If the marker is absent, that view did not execute. This single check can stop an unproductive investigation into stale Razor artifacts, browser caches, or copied files.

Next, enable informational logs for Microsoft.AspNetCore.Routing and Microsoft.AspNetCore.Mvc.Infrastructure in a development-only configuration. Look for the endpoint display name and the controller/action pair. The Executing endpoint entry establishes what the matcher selected; the MVC action log confirms what the framework invoked. Keep the test isolated and use fixed placeholders such as en, zh-TW, alpha, beta, and About. No external account, database, or service is needed.

A second diagnostic technique works when the returned page contains a distinctive, non-sensitive phrase. Search the local source tree for that phrase and identify the actual view or layout that owns it. This is especially effective when the incorrect page is structurally valid and therefore produces no exception. Do not copy request headers, user content, or environment values into the marker or search output.

Separate the investigation into three questions. First, can a route template match the path shape? Second, do its route constraints accept the extracted values? Third, which candidate endpoint does selection choose? A parameter named brand does not require a real brand record before routing can match it; without a constraint, About is merely another string. Conversely, rejecting a value with a route constraint removes that endpoint as a candidate but does not automatically make the request invalid. Another endpoint may still handle it.

Narrow the URL territory with constraints or literals

Use route constraints to describe the path values an endpoint owns, not as a replacement for application input validation. If the supported cultures are en and zh-TW, and the sample brands are alpha and beta, encode those boundaries. Then /en/alpha remains a brand request while /en/About can reach the conventional controller route. This removes the collision before controller execution.

[ApiController]
public class HomeController : ControllerBase
{
    [HttpGet("{culture:regex(^(en|zh-TW)$)}/{brand:regex(^(alpha|beta)$)}")]
    public IActionResult Brand(string culture, string brand)
        => Content($"BRAND:{culture}:{brand}");
}

public class AboutController : Controller
{
    public IActionResult Index(string culture)
        => Content($"ABOUT:{culture}");
}

Anchor regular expressions at the beginning and end so a partial match does not accidentally expand the accepted set. For a small, stable vocabulary, this is easy to review. For a larger or frequently changing set, use a named custom IRouteConstraint, a dynamic routing strategy designed for that requirement, or a clearer URL. Avoid embedding a large business data set in a regular expression.

A literal prefix is often the cleanest answer: /{culture}/brand/{brand} is visibly different from /{culture}/{controller}. It improves route comprehension, reduces future collisions when new controllers are added, and makes logs easier to interpret. Preserve the compact two-segment shape only when an established URL contract requires it. In that case, maintain an explicit collision list and regression tests for reserved controller names.

Constraint failure generally means that a particular endpoint is no longer a candidate. It does not inherently mean the application should return 400. If another endpoint matches, routing continues to that endpoint; if none matches, the typical result is 404. Data format errors, domain validation, and authorization belong in model binding, validators, filters, policies, or action logic where the application can produce the intended response contract.

During design review, ask three concrete questions for every parameter-heavy route: what exact URL space does it own, which values must it reject, and could a future controller or page name collide with those values? These questions are more useful than arguing about a single route registration order because they remain valid as the endpoint set grows.

Verification and reproduction with a fixed .NET 9 test project

The scheduler environment executed this non-destructive reproduction by building a net9.0 project with .NET SDK 10.0.400 and running it on the .NET 9.0.9 runtime. The unconstrained application returned BRAND:en:About for /en/About; the constrained application returned ABOUT:en. Both versions returned BRAND:en:alpha for the legitimate sample brand path. These are functional routing observations, not benchmark results.

  1. Create an empty ASP.NET Core Web project targeting net9.0. Add Controllers with Views, call MapControllers(), and map the conventional pattern {culture}/{controller}/{action=Index}.
  2. Add the unconstrained [HttpGet("{culture}/{brand}")] action and an AboutController.Index action. Build in Release mode, start the loopback-only test server, and request /en/About and /en/alpha. The reproduction passes when both responses identify the brand action.
  3. Replace the attribute template with [HttpGet("{culture:regex(^(en|zh-TW)$)}/{brand:regex(^(alpha|beta)$)}")], rebuild, and start a separate local instance.
  4. Request both paths again. The correction passes only when /en/About returns ABOUT:en and /en/alpha returns BRAND:en:alpha. A build failure, an unexpected endpoint marker, or an unsuccessful HTTP response fails the check.
dotnet --version
dotnet build --configuration Release
dotnet run --no-build --configuration Release --urls http://localhost:5181
curl --fail --silent http://localhost:5181/en/About
curl --fail --silent http://localhost:5181/en/alpha

Run the commands only in a local test project or isolated container. Bind the server to loopback, load no live configuration, and call no external API. The two variants should use different local ports if they run concurrently. Stop each process after collecting the response markers. This makes the procedure safe to repeat and avoids accidental interaction with any deployed site.

For long-term coverage, convert the same cases into integration tests with WebApplicationFactory or another in-process test host. Assert the status code, a positive endpoint marker, and the absence of the competing marker. The command-line reproduction is valuable because it has a small conceptual surface and exposes the selected endpoint clearly; it should complement, not replace, automated regression tests.

The observed output proves only the routing behavior represented by this minimal application and fixed target framework. It does not prove that every application has identical endpoint metadata. Custom matcher policies, dynamic controller routes, Razor Pages, localization middleware, and third-party routing components can change the candidate set. Recreate the application-specific endpoint map when those features are present.

Build a route matrix instead of checking one repaired URL

A one-page smoke test is insufficient. Repairing /en/About can accidentally break a legitimate brand URL, another culture, a three-segment action path, or the not-found contract. Define a route matrix that includes the root, every supported culture, an ordinary controller, a controller plus action, every reserved short path, one legal brand, one unknown brand, and a missing page. Each row should name the expected endpoint, status code, and stable response marker.

Prefer data-driven test cases over many copied test methods. For each case, assert at least three properties: the HTTP status follows the contract, the response contains the expected marker, and it does not contain the competing marker. A body-length check alone is weak because two unrelated pages can have similar lengths. A title can help, but a dedicated test marker is more stable when editorial copy changes.

Localization fallback must also be explicit. Decide whether an unsupported culture returns 404, redirects to a supported culture, or uses a default. Do not allow a broad route to make that policy accidental. Test en and zh-TW independently, then add one deliberately unsupported placeholder. The expected behavior should be visible in the test name and assertion rather than inferred from whichever endpoint happens to accept it.

Endpoint metadata can become another reviewable artifact. In a development or test environment, inspect EndpointDataSource and record each route pattern, display name, and order. Compare it with an approved snapshot when routes change. The goal is not to freeze framework internals forever; it is to make a newly introduced {x}/{y} or {*path} pattern visible before it quietly claims existing URLs. Keep the snapshot free of host names, user data, and runtime configuration.

When the route table changes, review both additions and removals. A removed constraint can be as dangerous as a new route. A renamed controller can release one path and collide with another parameter value. Treat the route matrix as an API contract even for server-rendered pages because search engines, bookmarks, internal links, and localized navigation all depend on stable URL semantics.

Safety, compatibility, and performance boundaries

A route constraint is not authorization. Restricting a URL to known sample brand names does not prove that a caller may access the selected resource. Authentication and authorization policies must still run at the correct layer. The examples intentionally contain no login data, private host details, payment flow, transaction, real account, or irreversible operation. Diagnostic logs should identify the endpoint without retaining complete request content.

This article checks current behavior against the official ASP.NET Core 9 routing and controller-routing documentation and executes a net9.0 sample. The endpoint-routing concepts apply across recent ASP.NET Core releases, but framework upgrades still require a rebuild and a complete route-matrix run. Do not treat one observation as a permanent guarantee across versions. In particular, custom MatcherPolicy implementations and dynamic routes deserve dedicated tests.

No performance benchmark was executed. It would be unsupported to claim that adding a regular-expression constraint always improves or harms latency. The demonstrated benefit is correctness and maintainability. If route matching becomes a measured concern in a large endpoint set, benchmark it separately with fixed hardware, framework version, route table, request distribution, warm-up policy, and tooling. Report observed data rather than turning a correctness fix into an invented speed claim.

Compatibility review should include URL generation as well as inbound matching. Links produced by tag helpers, LinkGenerator, redirects, and named routes must still generate the intended localized URL after constraints change. Test generation with legal values and confirm that rejected values do not silently generate a misleading URL. If the application exposes canonical and alternate-language links, validate those paths against the same matrix.

Operationally, return semantics matter. A wrongly selected endpoint that returns 200 can be cached, indexed, and linked as if it were correct. Therefore, monitoring should include representative content assertions for critical pages, not only reachability. Keep those assertions stable and generic; do not place private content in external monitoring probes.

Review checklist and conclusion

  • Every two-parameter or catch-all route has a documented URL territory, a literal prefix, or narrow constraints.
  • Route constraints filter endpoint candidates; validation, domain rules, authentication, and authorization remain in their proper layers.
  • Important paths assert status, the expected endpoint marker, and the absence of a competing marker.
  • The matrix covers legal brands, normal controllers, unsupported values, all supported cultures, action paths, and missing pages.
  • Tests pin the target framework and prerequisites, and they run again after an ASP.NET Core upgrade.
  • Endpoint diagnostics contain only necessary metadata and never depend on a live account or external service.
  • URL generation tests accompany inbound matching tests so links and redirects follow the same contract.

The practical conclusion is simple: HTTP success does not prove routing correctness when multiple templates can explain the same path. Identify the executed endpoint with a marker and framework logs, remove overlapping ownership with constraints or a literal prefix, and validate the boundary with a route matrix. That workflow turns a quiet wrong-page symptom into a repeatable contract that survives future controller and route additions.