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

67

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

当调用下面任何一个方法时,我会收到以下错误:

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)
{
    ...
}
7个回答

48

由于这些操作是动态激活的,所以你想做的事情是不可能的。请求数据(例如查询字符串)在框架了解操作签名之前无法绑定。它无法在跟踪路由之前知道操作签名。因此,您不能使路由依赖于框架甚至还不知道的东西。

简而言之,您需要以某种方式区分路由:要么是其他静态路径,要么使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)
{
    ...
}

6
“遗憾”的是,这在旧的ASP.NET on .NET Framework中可以正常工作。这是我在转换为ASP.NET Core时必须解决的障碍。 - Kevin
请求数据(例如查询字符串)在框架了解操作签名之前无法绑定。这似乎是对设计不佳的无意义借口,因为查询本身就是操作签名的一部分。正如Kevin指出的那样,在ASP.NET中是可以工作的。 - undefined

26

为避免路由冲突,动作路由需要是唯一的。

如果愿意更改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) {
    //...
}

##参考资料 在ASP.NET Core中路由到控制器操作

##参考资料 ASP.NET Core中的路由


1
谢谢。我按照你的示例更改了路由为api/menus/{menuId}/menuitems/users/{userId}。 - Magnus Wallström
看起来很聪明,但不幸的是遇到了“System.InvalidOperationException: 约束引用'int'无法解析为类型。使用'Microsoft.AspNetCore.Routing.RouteOptions.ConstraintMap'注册约束类型。”错误。有什么想法吗?我使用类似于[HttpGet("{id: int}")]的东西。 - user5871859
@Jonathan,那是我的笔误。冒号后面的空格要删除。我现在正在编辑答案。 - Nkosi
这个答案比被接受的答案更清晰。 - Ramil Aliyev 007

6

您的 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
这是另一种可用于此类情况的解决方案: 解决方案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。对于这种情况,它在下面(请注意,我正在尝试从查询字符串解析参数,但我正在使用一个区分符标头,该标头通常用于客户端和服务器之间的内容协商(内容协商):

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

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 应该转到接收整数参数的那个。


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

2

我遇到了这个错误,只需要重新启动服务就可以让它再次工作。可能是因为我在修改代码,它以某种方式重新注册了相同的控制器方法。


1
你可以拥有一个调度端点,它将从两个端点获取调用,并根据参数调用正确的端点。 (如果它们在同一个控制器中,则可以正常工作)。
示例:
// 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)
{
    ...
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接