如何解决请求与 .Net Core Web 中的多个端点匹配的问题 Api
How do I resolve the issue the request matched multiple endpoints in .Net Core Web Api
我注意到有很多关于此主题的类似问题。
我在调用以下任何方法时遇到此错误。
Microsoft.AspNetCore.Routing.Matching.AmbiguousMatchException: The request matched multiple endpoints.
但是我无法找出解决问题的最佳做法。
到目前为止,我还没有设置任何特定的路由中间件。
// 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)
{
...
}
您的 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)
{
...
}
操作路线需要唯一以避免路线冲突。
如果愿意更改 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) {
//...
}
您尝试执行的操作是不可能的,因为这些操作是动态激活的。在框架知道操作签名之前,无法绑定请求数据(例如查询字符串)。在遵循路线之前,它无法知道动作签名。因此,您不能使路由依赖于框架甚至还不知道的东西。
总而言之,您需要以某种方式区分路由:其他静态路径或使 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)
{
...
}
这是您可以用于此类情况的另一种解决方案:
解决方案 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,对于这种情况,它在下面(请参阅我正在尝试从查询字符串中解析参数,但我使用的是鉴别器 header 通常用于客户端和服务器之间的内容协商(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());
});
}
到目前为止你没有解决你遇到的问题,但我们已经接近了。为了立即触发控制器操作,您需要在请求中传递 header 类型: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);
}
还有这一个:
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参数的
在我的例子中 [HttpPost("[action]")]
被写了两次。
您可以有一个调度程序端点,它会从两个端点接收调用,并根据参数调用权限。
(如果它们在同一个控制器中,它会工作正常)。
示例:
// 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)
{
...
}
我遇到了这个错误,只需要重新启动服务就可以让它再次运行。可能是因为我正在修改代码,它 re-registered 以某种方式使用相同的控制器方法。
我注意到有很多关于此主题的类似问题。
我在调用以下任何方法时遇到此错误。
Microsoft.AspNetCore.Routing.Matching.AmbiguousMatchException: The request matched multiple endpoints.
但是我无法找出解决问题的最佳做法。 到目前为止,我还没有设置任何特定的路由中间件。
// 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)
{
...
}
您的 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)
{
...
}
操作路线需要唯一以避免路线冲突。
如果愿意更改 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) {
//...
}
您尝试执行的操作是不可能的,因为这些操作是动态激活的。在框架知道操作签名之前,无法绑定请求数据(例如查询字符串)。在遵循路线之前,它无法知道动作签名。因此,您不能使路由依赖于框架甚至还不知道的东西。
总而言之,您需要以某种方式区分路由:其他静态路径或使 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)
{
...
}
这是您可以用于此类情况的另一种解决方案:
解决方案 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,对于这种情况,它在下面(请参阅我正在尝试从查询字符串中解析参数,但我使用的是鉴别器 header 通常用于客户端和服务器之间的内容协商(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());
});
}
到目前为止你没有解决你遇到的问题,但我们已经接近了。为了立即触发控制器操作,您需要在请求中传递 header 类型: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);
}
还有这一个:
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参数的
在我的例子中 [HttpPost("[action]")]
被写了两次。
您可以有一个调度程序端点,它会从两个端点接收调用,并根据参数调用权限。 (如果它们在同一个控制器中,它会工作正常)。
示例:
// 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)
{
...
}
我遇到了这个错误,只需要重新启动服务就可以让它再次运行。可能是因为我正在修改代码,它 re-registered 以某种方式使用相同的控制器方法。