28

I notice that there are a bunch of similar questions out there about this topic.

I'm getting this error when calling any of the methods below.

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

I can't however sort out what is best practice in resolving the issue. So far I haven't set up any specific routing middleware.

// api/menus/{menuId}/menuitems
[HttpGet("{menuId}/menuitems")]
public IActionResult GetAllMenuItemsByMenuId(int menuId)
{            
    ....
}

// api/menus/{menuId}/menuitems?userId={userId}
[HttpGet("{menuId}/menuitems")]
public IActionResult GetMenuItemsByMenuAndUser(int menuId, int userId)
{
    ...
}
Nkosi
  • 191,971
  • 29
  • 311
  • 378
Magnus Wallström
  • 1,029
  • 4
  • 12
  • 23

6 Answers6

21

What you're trying to do is impossible because the actions are dynamically activated. The request data (such as a query string) cannot be bound until the framework knows the action signature. It can't know the action signature until it follows the route. Therefore, you can't make routing dependent on things the framework doesn't even know yet.

Long and short, you need to differentiate the routes in some way: either some other static path or making the userId a route param. However, you don't actually need separate actions here. All action params are optional by default. Therefore, you can just have:

[HttpGet("{menuId}/menuitems")]
public IActionResult GetMenuItemsByMenu(int menuId, int userId)

And then you can branch on whether userId == 0 (the default). That should be fine here, because there will never be a user with an id of 0, but you may also consider making the param nullable and then branching on userId.HasValue instead, which is a bit more explicit.

You can also continue to keep the logic separate, if you prefer, by utilizing private methods. For example:

[HttpGet("{menuId}/menuitems")]
public IActionResult GetMenuItems(int menuId, int userId) =>
    userId == 0 ? GetMenuItemsByMenuId(menuId) : GetMenuItemsByUserId(menuId, userId);

private IActionResult GetMenuItemsByMenuId(int menuId)
{
    ...
}

private IActionResult GetMenuItemsByUserId(int menuId, int userId)
{
    ...
}
Brien Foss
  • 2,950
  • 3
  • 16
  • 29
Chris Pratt
  • 207,690
  • 31
  • 326
  • 382
14

Action routes need to be unique to avoid route conflicts.

If willing to change the URL consider including the userId in the route

// api/menus/{menuId}/menuitems
[HttpGet("{menuId:int}/menuitems")]
public IActionResult GetAllMenuItemsByMenuId(int menuId)  
    //....
}

// api/menus/{menuId}/menuitems/{userId}
[HttpGet("{menuId:int}/menuitems/{userId:int}")]
public IActionResult GetMenuItemsByMenuAndUser(int menuId, int userId) {
    //...
}

##Reference Routing to controller actions in ASP.NET Core

##Reference Routing in ASP.NET Core

Nkosi
  • 191,971
  • 29
  • 311
  • 378
  • Thanks. I followed your example and changed the route to api/menus/{menuId}/menuitems/users/{userId} – Magnus Wallström Dec 12 '19 at 09:59
  • Seems smart, but unfortunately **"System.InvalidOperationException: The constraint reference ' int' could not be resolved to a type. Register the constraint type with 'Microsoft.AspNetCore.Routing.RouteOptions.ConstraintMap'."** error encountered. Any idea? I use something similar to `[HttpGet("{id: int}")]`. –  Oct 05 '20 at 04:59
  • @Jonathan that is a typo on my part. Remove the space after the colon. Editing answer now. – Nkosi Oct 05 '20 at 12:38
1

You have the same route in your HttpGet attribute

Change to something like this :

    // api/menus/{menuId}/menuitems
    [HttpGet("{menuId}/getAllMenusItems")]
    public IActionResult GetAllMenuItemsByMenuId(int menuId)
    {            
        ....
    }

    // api/menus/{menuId}/menuitems?userId={userId}
    [HttpGet("{menuId}/getMenuItemsFiltered")]
    public IActionResult GetMenuItemsByMenuAndUser(int menuId, int userId)
    {
        ...
    }
Laphaze
  • 179
  • 6
0

This is another solution that you can use for this kind of scenario:

Solution 1 and more complex, using IActionConstrain, and ModelBinders(this gives you the flexibility to bind your input to a specific DTO):

The problem you have is that your controller has the same routing for 2 different methods receiving different parameters. Let me illustrate it with a similar example, you can have the 2 methods like this:

Get(string entityName, long id)
Get(string entityname, string timestamp)

So far this is valid, at least C# is not giving you an error because it is an overload of parameters. But with the controller, you have a problem, when aspnet receives the extra parameter it doesn't know where to redirect your request. You can change the routing which is one solution.

Normally I prefer to keep the same names and wrap the parameters on a DtoClass, IntDto and StringDto for example

public class IntDto
{
    public int i { get; set; }
}

public class StringDto
{
    public string i { get; set; }
}
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    [HttpGet]
    public IActionResult Get(IntDto a)
    {
        return new JsonResult(a);
    }

    [HttpGet]
    public IActionResult Get(StringDto i)
    {
        return new JsonResult(i);
    }
}

but still, you have the error. In order to bind your input to the specific type on your methods, I create a ModelBinder, for this scenario, it is below(see that I am trying to parse the parameter from the query string but I am using a discriminator header which is used normally for content negotiation between the client and the server(Content negotiation):

public class MyModelBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (bindingContext == null)
            throw new ArgumentNullException(nameof(bindingContext));

        dynamic model = null;

        string contentType = bindingContext.HttpContext.Request.Headers.FirstOrDefault(x => x.Key == HeaderNames.Accept).Value;

        var val = bindingContext.HttpContext.Request.QueryString.Value.Trim('?').Split('=')[1];

        if (contentType == "application/myContentType.json")
        {

            model = new StringDto{i = val};
        }

        else model = new IntDto{ i = int.Parse(val)};

        bindingContext.Result = ModelBindingResult.Success(model);

        return Task.CompletedTask;
    }
}

Then you need to create a ModelBinderProvider (see that if I am receiving trying to bind one of these types, then I use MyModelBinder)

public IModelBinder GetBinder(ModelBinderProviderContext context)
        {
            if (context.Metadata.ModelType == typeof(IntDto) || context.Metadata.ModelType == typeof(StringDto))
                return new MyModelBinder();

            return null;
        }

and register it into the container

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers(options =>
        {
            options.ModelBinderProviders.Insert(0, new MyModelBinderProvider());
        });
    }

So far you didn't resolve the issue you have but we are close. In order to hit the controller actions now, you need to pass a header type on the request: application/json or application/myContentType.json. But in order to support conditional logic to determine whether or not an associated action method is valid or not to be selected for a given request, you can create your own ActionConstraint. Basically the idea here is to decorate your ActionMethod with this attribute to restrict the user to hit that action if he doesn't pass the correct media type. See below the code and how to use it

[AttributeUsage(AttributeTargets.All, Inherited = true, AllowMultiple = true)]
    public class RequestHeaderMatchesMediaTypeAttribute : Attribute, IActionConstraint
    {
        private readonly string[] _mediaTypes;
        private readonly string _requestHeaderToMatch;

        public RequestHeaderMatchesMediaTypeAttribute(string requestHeaderToMatch,
            string[] mediaTypes)
        {
            _requestHeaderToMatch = requestHeaderToMatch;
            _mediaTypes = mediaTypes;
        }

        public RequestHeaderMatchesMediaTypeAttribute(string requestHeaderToMatch,
            string[] mediaTypes, int order)
        {
            _requestHeaderToMatch = requestHeaderToMatch;
            _mediaTypes = mediaTypes;
            Order = order;
        }

        public int Order { get; set; }

        public bool Accept(ActionConstraintContext context)
        {
            var requestHeaders = context.RouteContext.HttpContext.Request.Headers;

            if (!requestHeaders.ContainsKey(_requestHeaderToMatch))
            {
                return false;
            }

            // if one of the media types matches, return true
            foreach (var mediaType in _mediaTypes)
            {
                var mediaTypeMatches = string.Equals(requestHeaders[_requestHeaderToMatch].ToString(),
                    mediaType, StringComparison.OrdinalIgnoreCase);

                if (mediaTypeMatches)
                {
                    return true;
                }
            }

            return false;
        }
    }

Here is your final change:

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    [HttpGet]
    [RequestHeaderMatchesMediaTypeAttribute("Accept", new[] { "application/json" })]
    public IActionResult Get(IntDto a)
    {
        return new JsonResult(a);
    }

    [RequestHeaderMatchesMediaTypeAttribute("Accept", new[] { "application/myContentType.json" })]
    [HttpGet]
    public IActionResult Get(StringDto i)
    {
        return new JsonResult(i);
    }
}

Now the error is gone if you run your app. But how you pass the parameters?: This one is going to hit this method:

public IActionResult Get(StringDto i)
        {
            return new JsonResult(i);
        }

application/myContentType.json

And this one the other one:

 public IActionResult Get(IntDto a)
        {
            return new JsonResult(a);
        }

application/json

Solution 2: Routes constrains

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    [HttpGet("{i:int}")]
    public IActionResult Get(int i)
    {
        return new JsonResult(i);
    }

    [HttpGet("{i}")]
    public IActionResult Get(string i)
    {
        return new JsonResult(i);
    }
}

This is a kind of test because I am using the default routing:

https://localhost:44374/weatherforecast/"test"  should go to the one that receives the string parameter

https://localhost:44374/weatherforecast/1 should go to the one that receives an int parameter

Zinov
  • 2,956
  • 4
  • 26
  • 55
0

In my case [HttpPost("[action]")] was written twice.

Aneeq Azam Khan
  • 694
  • 1
  • 8
  • 19
0

You can have a dispatcher endpoint that will get the calls from both endpoints and will call the right based on parameters. (It will works fine if their are in same controller).

Example:

// api/menus/{menuId}/menuitems
[HttpGet("{menuId}/menuitems")]
public IActionResult GetAllMenuItemsByMenuId(int menuId, int? userId)
{            
    if(userId.HasValue)
       return GetMenuItemsByMenuAndUser(menuId, userId)
.... original logic
}

public IActionResult GetMenuItemsByMenuAndUser(int menuId, int userId)
{
    ...
}
Ygalbel
  • 4,049
  • 18
  • 28