如何解决请求与.Net Core Web Api中的多个终结点匹配的问题

时间:2019-12-11 10:03:03

标签: c# .net-core asp.net-core-webapi asp.net-core-routing

我注意到关于该主题还有很多类似的问题。

调用下面的任何方法时都会收到此错误。

  

Microsoft.AspNetCore.Routing.Matching.AmbiguousMatchException:该请求匹配了多个端点。

但是,我无法解决解决此问题的最佳实践。 到目前为止,我还没有设置任何特定的路由中间件。

// 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)
{
    ...
}

6 个答案:

答案 0 :(得分:2)

动作路由必须唯一,以避免路由冲突。

如果愿意更改URL,请考虑在路由中包含userId

// 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) {
    //...
}

参考Routing to controller actions in ASP.NET Core

参考Routing in ASP.NET Core

答案 1 :(得分:2)

您要执行的操作是不可能的,因为这些操作是动态激活的。在框架知道动作签名之前,无法绑定请求数据(例如查询字符串)。只有遵循路线,它才能知道动作签名。因此,您不能使路由依赖于框架甚至不知道的事情。

长短不一,您需要以某种方式区分路由:使用其他静态路径或将userId设置为路由参数。但是,您实际上不需要在这里执行单独的操作。默认情况下,所有操作参数都是可选的。因此,您可以拥有:

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

然后,您可以分支是否设置userId == 0(默认值)。在这里应该没问题,因为ID的用户永远不会为0,但是您也可以考虑将参数设为可空,然后分支到userId.HasValue上,这更加明确。

如果愿意,还可以通过使用私有方法继续将逻辑分开。例如:

[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)
{
    ...
}

答案 2 :(得分:0)

您的HttpGet属性中的路由相同

更改为以下内容:

    // 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)
    {
        ...
    }

答案 3 :(得分:0)

这是可用于这种情况的另一种解决方案:

使用IActionConstrain和ModelBinders解决方案1和更复杂的方法(这使您可以灵活地将输入绑定到特定的DTO):

您遇到的问题是,对于具有不同参数的2种不同方法,您的控制器具有相同的路由。 让我用一个类似的示例进行说明,您可以使用以下两种方法:

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

到目前为止,这是有效的,至少C#没有给您错误,因为它是参数的重载。但是对于控制器,您有一个问题,当aspnet收到额外的参数时,它不知道将请求重定向到何处。 您可以更改路由,这是一种解决方案。

通常,我更喜欢保持相同的名称,并将参数包装在DtoClass,IntDto和StringDto上

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);
    }
}

但是仍然有错误。为了将您的输入绑定到方法上的特定类型,我创建了一个ModelBinder,在这种情况下,如下所示(请参阅我试图从查询字符串中解析参数,但我使用的是鉴别符头通常用于客户端和服务器之间的内容协商(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;
    }
}

然后,您需要创建一个ModelBinderProvider(请参见,如果我收到试图绑定这些类型之一的文件,那么我将使用MyModelBinder)

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

            return null;
        }

并将其注册到容器中

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

到目前为止,您尚未解决所遇到的问题,但我们已经结束。为了立即执行控制器操作,您需要在请求上传递标头类型: application / json application / myContentType.json 。但是,为了支持条件逻辑来确定关联的操作方法对于给定的请求是否有效,您可以创建自己的ActionConstraint。基本上,这里的想法是用此属性装饰您的ActionMethod,以限制用户在未传递正确媒体类型的情况下执行该操作。参见下面的代码及其用法

[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;
        }
    }

这是您的最终更改:

[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);
    }
}

现在,如果您运行应用程序,该错误将消失。但是如何传递参数?: 这个将要使用这种方法:

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

application/myContentType.json

这个又一个:

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

application/json

解决方案2:路线受限

[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);
    }
}

这是一种测试,因为我使用的是默认路由:

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

https://localhost:44374/weatherforecast/1应该转到接收一个int参数的

答案 4 :(得分:0)

在我的情况下,[HttpPost("[action]")]被写了两次。

答案 5 :(得分:0)

您可以有一个调度程序端点,该端点将接收来自这两个端点的调用并根据参数调用正确的权限。 (如果它们在同一控制器中,它将很好地工作。)

示例:

// 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)
{
    ...
}