Search code examples
c#asp.net-corerazor-pages

Why do I get this AmbiguousMatchException?


I am writing a simple .NET 7 web application that will respond to any GET request by serving the same Razor page. This is my Program.cs:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.MapFallbackToPage("/Index");
app.Run();

A request to https://localhost:7024/ returns my Razor page as expected. However, a request to https://localhost:7024/anythingelse throws an exception:

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

/Index
/Index

at Microsoft.AspNetCore.Routing.Matching.DefaultEndpointSelector.ReportAmbiguity(CandidateState[] candidateState) at Microsoft.AspNetCore.Routing.Matching.DefaultEndpointSelector.ProcessFinalCandidates(HttpContext httpContext, CandidateState[] candidateState) at Microsoft.AspNetCore.Routing.Matching.DefaultEndpointSelector.SelectAsync(HttpContext httpContext, CandidateSet candidateSet) at Microsoft.AspNetCore.Routing.Matching.DfaMatcher.SelectEndpointWithPoliciesAsync(HttpContext httpContext, IEndpointSelectorPolicy[] policies, CandidateSet candidateSet) at Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware.g__AwaitMatch|8_1(EndpointRoutingMiddleware middleware, HttpContext httpContext, Task matchTask) at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)


Solution

  • My initial answer did not address your question: "Why do I get this...?" It just provided how to resolve the AmbiguousMatchException error. Here's an explanation of why this error occurs, along with a code sample that shows what's happening.

    ASP.NET Core is creating multiple routes for /Index: 1. for the Razor Page with the file name Index.cshtml; and 2. for the root URL of the web app (or https://localhost:7024/ in your question).

    enter image description here

    Navigating to https://localhost:7024/ or https://localhost:7024/index works because these two route patterns have a clear match.

    When app.MapFallbackToPage("/Index"); is set in program.cs, with the first parameter set to the page name: /Index, the request is routed to the page endpoint /Index. The ambiguous exception occurs because there are two routes that match the endpoint for /Index.

    Add a catch-all route template

    Adding a catch-all route template (i.e. starting a route template with an asterik * followed by any word) as @Mike added in his answer, removes ambiguity.

    @page "{*anyword}"
    

    The catch-all route pattern, the second list item in the image below, matches https://localhost:7024/anythingelse.

    Using a catch-all route pattern, however, in the @page directive in the Index.cshtml file in the root folder makes app.MapFallbackToPage("/Index"); redundant. All request paths will be processed by the catch-all route pattern.

    enter image description here

    Add "/Index" template

    Add "/Index" to the @page directive in the main index page also removes ambiguity. Only one endpoint to /Index is registered.

    @page "/Index"
    @model WebApplication1.Pages.IndexModel
    @{
    }
    <h1>Welcome</h1>
    

    enter image description here

    EndpointDataList code sample

    The following code sample was used generate the image results above. Replace the first line in Index.cshtml, that is, the @page directive with any of the following to get the output of the images above, in order:

    @page
    @page "{*anyword}"
    @page "/Index"
    

    Index.cshtml

    @page "/Index"
    @model WebApplication1.Pages.IndexModel
    <table class="table">
        <thead>
            <tr>
                <th>Display Name</th>
                <th>Route Pattern</th>
                <th>Url</th>
            </tr>
        </thead>
        <tbody>
            @foreach (EndpointData endpointData in @Model.EndpointDataList)
            {
                <tr>
                    <td>@endpointData.DisplayName</td>
                    <td>@endpointData.RoutePattern</td>
                    <td>@endpointData.URL</td>
                </tr>
            }
        </tbody>
    </table>
    

    Index.cshtml.cs

    using Microsoft.AspNetCore.Mvc.RazorPages;
    
    namespace WebApplication1.Pages
    {
        public class IndexModel : PageModel
        {
            private readonly IEnumerable<EndpointDataSource> _endpointSources;
            public IndexModel(
                IEnumerable<EndpointDataSource> endpointDataSources)
            {
                _endpointSources = endpointDataSources;
            }
    
            public IEnumerable<RouteEndpoint> EndpointSources { get; set; }
            public List<EndpointData> EndpointDataList { get; set; }
    
            public void OnGet()
            {
                EndpointSources = _endpointSources
                            .SelectMany(es => es.Endpoints)
                            .OfType<RouteEndpoint>();
    
                EndpointDataList = new List<EndpointData>();
    
                foreach (RouteEndpoint routeEndpoint in EndpointSources)
                {
                    // If the route pattern is an empty string then
                    // it represents the home page. The home page is
                    // represented by the route pattern '/Index'.
    
                    EndpointData pageData = new()
                    {
                        DisplayName = routeEndpoint.DisplayName,
                        RoutePattern = routeEndpoint.RoutePattern.RawText,
                        URL = routeEndpoint.ToString()
                    };
                    EndpointDataList.Add(pageData);
                }
            }
        }
    
        public class EndpointData
        {
            public string DisplayName { get; set; }
            public string RoutePattern { get; set; }
            public string URL { get; set; }
        }
    }
    

    Program.cs

    var builder = WebApplication.CreateBuilder(args);
    builder.Services.AddRazorPages();
    
    var app = builder.Build();
    
    app.UseHttpsRedirection();
    app.UseStaticFiles();
    app.UseRouting();
    app.UseAuthorization();
    app.MapRazorPages();
    app.MapFallbackToPage("/Index");
    app.Run();