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

Posted

技术标签:

【中文标题】如何解决请求与 .Net Core Web Api 中的多个端点匹配的问题【英文标题】:How do I resolve the issue the request matched multiple endpoints in .Net Core Web Api 【发布时间】:2020-04-04 13:59:39 【问题描述】:

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

我在调用以下任何方法时收到此错误。

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)

    ...

【问题讨论】:

【参考方案1】:

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

无论长短,您都需要以某种方式区分路由:要么是其他静态路径,要么将 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】:

动作路线需要是唯一的以避免路线冲突。

如果愿意更改 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

【讨论】:

谢谢。我按照您的示例将路线更改为 api/menus/menuId/menuitems/users/userId 看起来很聪明,但不幸的是 "System.InvalidOperationException:约束引用“int”无法解析为类型。使用“Microsoft.AspNetCore.Routing.RouteOptions.ConstraintMap”注册约束类型'." 遇到错误。任何想法?我使用类似于[HttpGet("id: int")] 的东西。 @Jonathan 这是我的错字。删除冒号后面的空格。现在编辑答案。【参考方案3】:

您的 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)
    
        ...
    

【讨论】:

【参考方案4】:

在我的例子中,[HttpPost("[action]")] 被写了两次。

【讨论】:

【参考方案5】:

这是可用于此类场景的另一种解决方案:

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

您遇到的问题是您的控制器对于接收不同参数的 2 种不同方法具有相同的路由。 让我用一个类似的例子来说明它,你可以有这样的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 StringDtoi = 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/jsonapplication/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);
        

这个是另一个:

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

解决方案 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 参数的那个

【讨论】:

【参考方案6】:

您可以拥有一个调度程序端点,该端点将从两个端点获取调用,并根据参数调用权限。 (如果它们在同一个控制器中,它将正常工作)。

例子:

// 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)

    ...

【讨论】:

以上是关于如何解决请求与 .Net Core Web Api 中的多个端点匹配的问题的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 Angular 取消 .Net Core Web API 请求?

ASP.NET Core Web API - PUT 与 PATCH

如何在 ASP.NET Core MVC 中请求和显示实时 Web api 数据?

使用 .NET Core Web API 登录和注册

ASP.NET Core Web API 身份验证

如何停止 .Net Core Web API 中的自引用循环?