Aspnetcore: Using MapFallbackToPage with a page called Index.cshtml leads to an ambiguious match

Created on 29 Jul 2019  路  8Comments  路  Source: dotnet/aspnetcore

Repro

  1. Create a new Server-Side Blazor app.
  2. Rename Pages/_Host.cshtml -> Pages/Index.cshtml.
  3. Navigate to /Index in the app.

This results in an ambiguous match from routing because a page named Index produces two endpoints. This isn't ideal because it can be a useful pattern to place a page in the app route and then use fallback routing.

area-mvc bug feature-routing

Most helpful comment

Well... I think I just ran into it this. Been pulling my hair out trying to figure out how I was getting 2 endpoints for a fallback to a razor page to serve a simple spa.

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();    
        endpoints.MapFallbackToPage("/Index");
    });

If / is requested it doesn't reach the fallback and is served... however... any request expected to be handled by the client hits the fallback and the following error is generated:

Microsoft.AspNetCore.Routing.Matching.AmbiguousMatchException: The request matched multiple endpoints. Matches: 
/Index
/Index

All 8 comments

This seems low priority for right now, but was worth logging. If users hit this in practice it should bump up the priority.

That's where I hit it. You have to navigate to /Index.

Well... I think I just ran into it this. Been pulling my hair out trying to figure out how I was getting 2 endpoints for a fallback to a razor page to serve a simple spa.

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();    
        endpoints.MapFallbackToPage("/Index");
    });

If / is requested it doesn't reach the fallback and is served... however... any request expected to be handled by the client hits the fallback and the following error is generated:

Microsoft.AspNetCore.Routing.Matching.AmbiguousMatchException: The request matched multiple endpoints. Matches: 
/Index
/Index

I was trying to do the same thing. After some intense code diving I came up with a work around. I'm not very intimate with the internals of ASP.Net Core so I can't promise it's going to be good on the routing middleware, but here is what I did if you're also looking for a work around:

public class ResolvePageActionAmbiguityMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy
{
  public bool AppliesToEndpoints(IReadOnlyList<Endpoint> endpoints)
  {
    return true;
  }

  public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates)
  {
    if (candidates.Count < 2)
    {
      // Quick exit when there aren't enough candidates to even have an ambiguity.
      return Task.CompletedTask;
    }

    int? score = null;
    PageActionDescriptor? firstDescriptor = null;
    for (var i = 0; i < candidates.Count; ++i)
    {
      if (!candidates.IsValidCandidate(i))
      {
        continue;
      }

      ref var state = ref candidates[i];
      if (state.Score > score)
      {
        // Only the lowest score is going to matter so don't waste time resolving higher scoring ambiguities.
        continue;
      }

      var pageActionDescriptor = candidates[i].Endpoint.Metadata.GetMetadata<PageActionDescriptor>();
      if (firstDescriptor == null || state.Score < score)
      {
        score = state.Score;
        firstDescriptor = pageActionDescriptor;
        continue;
      }

      // Remove ambiguities that have the exact same view engine and relative paths. Just use the first one.
      // Note: Some resolutions could be wasted work if the candidate set is not ordered by score.
      if (
        string.Equals(firstDescriptor.ViewEnginePath, pageActionDescriptor.ViewEnginePath, StringComparison.Ordinal) &&
        string.Equals(firstDescriptor.RelativePath, pageActionDescriptor.RelativePath, StringComparison.Ordinal)
      )
      {
        candidates.ReplaceEndpoint(i, null, null);
      }
    }

    return Task.CompletedTask;
  }

  public override int Order { get; } = int.MaxValue - 100;
}

Register this in Startup:

services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, ResolvePageActionAmbiguityMatcherPolicy>());

This will resolve ambiguity when more than one route exists for the same page as best as I can tell.

"Index" is a magic name in MVC. The MVC middleware is creating two route paths for Index so that you can leave the word "Index" off the URL and end up at the same page action. That is just fine for going from URL to Page, but creates ambiguity when going from Page to URL. It turns out that either route path gets you to the same place you're trying to fallback to, so it really doesn't matter which one is used (I think).

At least, that's what I think is happening. I'm no expert on the subject.

Note: I wrote this in C# 8 with nullable reference types enabled. If you aren't using nullable reference types, you'll have to make some adjustments.

We are using Vue.js with .net core and originally used @shand-obs solution, which worked until we upgraded to the .net core version 3.1.3 runtime. Then it started throwing the same error as before.
We knew if we created a copy of Index and named it something else we wouldnt hit the issue, but then we would have to track any changes to the main page of our SPA in 2 different files, which would be a pain. We then came up with a novel hack to use webpack to create a copy of the Index.cshtml (Razor page) and name it "Dashboard":

new HtmlWebpackPlugin({ 
    inject: false,
    template: 'Pages/Index.cshtml',
    filename: 'Pages/Dashboard.cshtml', // <-- copy
})

then in startup.cs we point to that copy of the Index Razor page for the fallback:

endpoints.MapFallbackToPage("/Dashboard");

even though the names are different it uses the same code behind file because it is referenced in the Razor page like this (both the original Index and the duplicate Dashboard):

@page
@model IndexModel // <-- same code behind in both files

so in deployment webpack makes a copy of the Index page just to trick the MapFallbackToPage() into thinking that it is not the same route for the SPA, even though it is exactly the same code. We could have done the same thing by just manually creating a copy, but this way we dont have to keep track of changes in 2 files. This solved the issue for us, let me know if you see any issues with this or if this helps you.

also, we added authorization to both routes:

services.AddRazorPages()
    .AddRazorPagesOptions(options =>
     {
         options.Conventions.AuthorizePage("/Index");
         options.Conventions.AuthorizePage("/Dashboard");
     });

We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.

Was this page helpful?
0 / 5 - 0 ratings