The request matched multiple endpoints but why?

13,135

Solution 1

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.

This solution gives you the ability to map your input to a complex type as well, otherwise use Route constraint for simple types

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

Run it and let me know

Solution 2

You can fix this using route constraints.

Take a look at https://docs.microsoft.com/en-us/aspnet/web-api/overview/web-api-routing-and-actions/attribute-routing-in-web-api-2

Here's their example:

[Route("users/{id:int}")]
public User GetUserById(int id) { ... }

[Route("users/{name}")]
public User GetUserByName(string name) { ... }
Share:
13,135

Related videos on Youtube

kafka
Author by

kafka

Updated on May 26, 2022

Comments

  • kafka
    kafka almost 2 years

    I have a controller that has multiple routes.

    I am trying to call an endpoint stated as

    GET: api/lookupent/2020-03-17T13:28:37.627691
    

    but this results in this error

    Microsoft.AspNetCore.Routing.Matching.AmbiguousMatchException: The request matched multiple endpoints. Matches: 
    
    Controllers.RecordController.Get (API)
    Controllers.RecordController.GetRecordRegisteredAt (API)
    

    but I am not sure I understand why this makes sense since this code

    // GET: api/{RecordName}/{id}
    [HttpGet("{RecordName}/{id}", Name = "GetRecord")]
    public ActionResult Get(string RecordName, long id)
    
    
    // GET: api/{RecordName}/{timestamp}
    [HttpGet("{RecordName}/{timestamp}", Name = "GetRecordRegisteredAt")]
    public ActionResult GetRecordRegisteredAt(string RecordName, string timestamp)
    

    why does the input match with these endpoints?