I have a WebApi using .NET core 3.1. I'm not sure if this isn't best practice or if there's another way to do this. But, I have a WebApi that will have 3 get verbs.
1. Get All.
2. Get by Id.
3. Get by a search string.
It work if I have just 2, the get all and get by id but once I had a 3rd, get by string I get an error. Then when I go to test in swagger I get,
Microsoft.AspNetCore.Routing.Matching.AmbiguousMatchException: The
request matched multiple endpoints. Matches:
Code:
[ApiController]
[Route("api/v1/[controller]")]
public class ProfessionalLocalController : ControllerBase
{
private IUnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public ProfessionalLocalController(IUnitOfWork unitOfWork, IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
[HttpGet]
public async Task<IActionResult> Get()
{
var profLocals = await _unitOfWork.ProfessionalLocalUsers.GetAllAsync();
return Ok(_mapper.Map<IEnumerable<ProfessionalLocalDto>>(profLocals));
}
[HttpGet("{id}")]
public async Task<IActionResult> Get(int id)
{
var localUser = await _unitOfWork.ProfessionalLocalUsers.GetAsync(id);
if (localUser == null)
{
return NotFound();
}
return Ok(_mapper.Map<ProfessionalLocalDto>(localUser));
}
[HttpGet("{searchText}", Name = "Search")]
public async Task<IActionResult> Get(string searchText)
{
var localUsers = await _unitOfWork.ProfessionalLocalUsers.FindAsync(temp => temp.UserID.ToString().Contains(searchText));
return Ok(_mapper.Map<IEnumerable<ProfessionalLocalDto>>(localUsers));
}
}
Routes need to be unique when mapped to controller actions.
In this case the routes [HttpGet("{id}")] and [HttpGet("{searchText}", Name = "Search")] conflict because to both map to the same URI.
api/v1/ProfessionalLocal/{some value here}
If you want to keep the same URI as before, and allow for the the two action to be distinct, use a route constraint.
//GET api/v1/ProfessionalLocal/1234
[HttpGet("{id:int}")] //<-- note the constraint on the id
public async Task<IActionResult> Get(int id) {
//...
}
So that the above is called only when a valid value is provided. An integer in this case
And consider changing the route template of the search to be more descriptive of what it actually does.
//GET api/v1/ProfessionalLocal/search/{search text here}
[HttpGet("search/{searchText}", Name = "Search")]
public async Task<IActionResult> Get(string searchText) {
//...
}
Reference Routing to controller actions in ASP.NET Core
Reference Routing in ASP.NET Core
You can try changing it like below:
[HttpGet]
[Route("users")]
public async Task<IActionResult> Get()
{
var profLocals = await _unitOfWork.ProfessionalLocalUsers.GetAllAsync();
return Ok(_mapper.Map<IEnumerable<ProfessionalLocalDto>>(profLocals));
}
[HttpGet("{id}")]
[Route("users/{id}")]
public async Task<IActionResult> Get(int id)
{
var localUser = await _unitOfWork.ProfessionalLocalUsers.GetAsync(id);
if (localUser == null)
{
return NotFound();
}
return Ok(_mapper.Map<ProfessionalLocalDto>(localUser));
}
[HttpGet("{searchText}", Name = "Search")]
[Route("usersbytext/{id}")]
public async Task<IActionResult> Get(string searchText)
{
var localUsers = await _unitOfWork.ProfessionalLocalUsers.FindAsync(temp => temp.UserID.ToString().Contains(searchText));
return Ok(_mapper.Map<IEnumerable<ProfessionalLocalDto>>(localUsers));
}
Add a custom route for third one.
The default routing is unable to distinguish [Route("users/{id}")] from [Route("users/{id}")] making it ambigous.
I ran into a similar problem recently. My solution was to use the same method for GetAll and Search. What I did was:
Check for the query parameter, if the query parameter is available, return search results else return all. Please refer to the snippet below.
[HttpGet]
public async Task<IActionResult> Get([FromQuery]string search)
{
if(!String.IsNullOrEmpty(search))
// return search result
//return all
}
I use this
[HttpGet("[action]")]
public async Task<IActionResult> GetAll()
{
var profLocals = await _unitOfWork.ProfessionalLocalUsers.GetAllAsync();
return Ok(_mapper.Map<IEnumerable<ProfessionalLocalDto>>(profLocals));
}
Related
The idea is that with these ItemController methods I should be able to:
Get the items by barcode when hitting "item/query?barcode={barcode}"
Get the items by discount when hitting "item/query?discount={discount}"
Get the items by name when hitting "item/query?name={name}"
Get the items by category when hitting "item/query?category={category}"
And this is my code for actions under the GET verb:
[HttpGet("query/{barcode:int}")]
public async Task<IActionResult> GetByBarcode(int barcode)
{
var item = ...
return Ok(item);
}
[HttpGet("query/{discount:int}")]
public async Task<IActionResult> GetByDiscount(int discount)
{
var items = ...
return Ok(items);
}
[HttpGet("query/{name}")]
public async Task<IActionResult> GetByName(string name)
{
var items = ...
return Ok(items);
}
[HttpGet("query/{category}")]
public async Task<IActionResult> GetByCategory(string category)
{
var items = ...
return Ok(items);
}
The problem is that I keep getting 405 Method Not Allowed when I try to access to any of those actions. I don't know if it's a matter of ambiguity between the methods, can you help me out?
Edit:
Header of ItemController.cs:
using InventoryWebApi.DataAccess;
using InventoryWebApi.DTO;
using InventoryWebApi.Models;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace InventoryWebApi.Controllers
{
[ApiController]
[Route("[controller]")]
public class ItemController : ControllerBase
{
private readonly IRepository repository;
public ItemController(IRepository repository)
...
you are using routing a wrong way.
Your code will not be even run,
your actions shoud be
[HttpGet("query/getbybarcode/{barcode:int})]
public async Task<IActionResult> GetByBarcode(int barcode)
[HttpGet("query/getbydiscount/{discount:int}")]
public async Task<IActionResult> GetByDiscount(int discount)
For this routes you have to use this urls
.../item/query/getbybarcode/{barcode}
..../item/query/getbydiscount/{discount}
if you still want to create url your way you will have to change attribute routing
[HttpGet("query/getbybarcode)]
public async Task<IActionResult> GetByBarcode(int barcode)
[HttpGet("query/getbydiscount")]
public async Task<IActionResult> GetByDiscount(int discount)
and your url should be
.../item/query/getbydiscount?discount={discount}"
..../item/query/getbybarcode?barcode={barcode}"
Update
If you still need to use all your urls for your student project , then the only way is to use one action for all your urls
[HttpGet("query")]
public async Task<IActionResult> Get(int? barcode, int? discount,
string name, string category )
{
if (barcode != null) ...
else if (discount!=null) ...
var item = ...
return Ok(item);
}
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?
You can fix this using route constraints.
Take a look at https://learn.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) { ... }
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);
}
And this one the other one:
public IActionResult Get(IntDto a)
{
return new JsonResult(a);
}
Run it and let me know
I had the same issue for these two methods:
[HttpPost]
public async Task<IActionResult> PostFoos(IEnumerable<FooModelPostDTO> requests)
[HttpPost]
public async Task<IActionResult> GetFoos(GetRequestDTO request)
The first one is for getting entities (using Post) and the second one is for posting new entities in DB (again using Post).
One possible solution is to distinguish between them by their's method names (../[action]) with the Route attribute:
[Route("api/[controller]/[action]")]
[ApiController]
public class FoosController : ControllerBase
I have a controller that should return full urls of the other Actions in the same controller. I dont know how I can do this. Please look at my example (using pseudo results):
[Route("api/v{api-version:apiVersion}/Tests")]
[ApiController][AllowAnonymous]
public class TestsController : ControllerBase
{
[HttpGet]
public async Task<IActionResult> GetActions()
{
var idDemo = 1;
//TODO
urls.Add("Get full url for Call method with parameter : idDemo ");
urls.Add("Get full url for Something method with parameter : idDemo ");
return Ok(urls);
}
[HttpPost("call/{id}")]
public async Task<IActionResult> Call(int id)
{
return Ok();
}
[HttpPost("do/it/{id}")]
public async Task<IActionResult> Something(int id)
{
return Ok();
}
}
I'm trying to get a REST service up and running (I followed this tutorial), and was trying to extend it with a simple method to mark one of the ToDoItem as "Complete"; literally to pass an ID into a method which should mark it as "Complete".
However, I'm struggling to understand how the routing works.
This is the method provided by default, which works correctly via https://localhost:44388/api/values
If I add another GET operation, even with different [Route] attribute, then I end up with "AmbiguousActionException: Multiple actions matched"
[Route("api/values")]
[ApiController]
public class ValuesController : ControllerBase
{
// GET api/values
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
return new string[] { "value1", "value2" };
}
I tried to specify a route prefix using the method below, so that I could add doesn't work; I get a 404 on https://localhost:44388/api/values and https://localhost:44388/api/values/getbyname
[RoutePrefix("api/values")]
[ApiController]
public class ValuesController : ControllerBase
{
// GET api/values
[Route("getbyname")]
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
return new string[] { "value1", "value2" };
}
I might be trying the wrong method, so I'm happy to take any advice. I just want to be able to create new REST calls and have them the appropriate actions. Do I need to create other controllers? Am I limited to one GET/POST/PUT etc per controller?
Edit: didn't provide enough info, here's more code:
[Route("api/ToDo")]
[ApiController]
public class ToDoController : ControllerBase
{
private readonly ToDoContext _context;
public ToDoController(ToDoContext toDoContext)
{
_context = toDoContext;
if (_context.ToDoItems.Count() == 0)
{
//collection is empty, so add a new item
ToDoItem item1 = new ToDoItem(1, "example 1");
ToDoItem item2 = new ToDoItem(2, "example 2");
_context.ToDoItems.Add(item1);
_context.ToDoItems.Add(item2);
_context.SaveChanges();
}
}
//GET: api/todo
[HttpGet]
public async Task<ActionResult<IEnumerable<ToDoItem>>> GetToDoItems()
{
return await _context.ToDoItems.ToListAsync();
}
//GET: api/todo/5
//[HttpGet(Name = "Get a ToDoItem")]
//[Route("get{id}")]
[HttpGet("{id}")]
public async Task<ActionResult<ToDoItem>> GetToDoItem(long id)
{
var todoitem = await _context.ToDoItems.FindAsync(id);
if (todoitem == null)
{
return NotFound();
}
return todoitem;
}
//POST: api/Todo
[HttpPost]
public async Task<ActionResult<ToDoItem>> PostToDoItem(ToDoItem todoItem)
{
_context.ToDoItems.Add(todoItem);
await _context.SaveChangesAsync();
//calls the "GetToDoItem" method above!
return CreatedAtAction("GetToDoItem", new { id = todoItem.ID }, todoItem);
}
//DELETE: api/todo/5
[HttpDelete("{id}")]
public async Task<ActionResult<ToDoItem>> DeleteToDoItem(long id)
{
var todoItem = await _context.ToDoItems.FindAsync(id);
if(todoItem == null)
{
return NotFound();
}
_context.ToDoItems.Remove(todoItem);
await _context.SaveChangesAsync();
return todoItem;
}
//* -. space to create a "MarkAsComplete" method
//GET: api/todo/5
[HttpGet(Name = "{name}")]
public async Task<ActionResult<ToDoItem>> MarkAsComplete(long id)
{
var todoitem = await _context.ToDoItems.FindAsync(id);
if (todoitem == null)
{
return NotFound();
}
else
{
todoitem.IsComplete = true;
}
return todoitem;
}
//*/
}
Mixing up different versions of the attributes. RoutePrefix is from a previous version.
Routes need to be unique per action to avoid route conflicts.
For example.
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase {
// GET api/values
[HttpGet]
public ActionResult<IEnumerable<string>> Get() {
return new string[] { "value1", "value2" };
}
// GET api/values/some_name
[HttpGet("{name}")]
public IActionResult GetByName(string name) {
return Ok();
}
}
Reference Routing to controller actions in ASP.NET Core
When building a REST API, it's rare that you will want to use [Route(...)] on an action method. It's better to use the more specific Http*Verb*Attributes to be precise about what your API supports. Clients of REST APIs are expected to know what paths and HTTP verbs map to specific logical operations.
Based on the additional details provided, that MarkAsComplete action should use HTTP PUT so signify that the model is being edited/updated.
For example
//* -. space to create a "MarkAsComplete" method
//PUT: api/todo/5
[HttpPut("{id:long}")]
public async Task<ActionResult<ToDoItem>> MarkAsComplete(long id) {
var todoitem = await _context.ToDoItems.FindAsync(id);
if (todoitem == null) {
return NotFound();
} else {
todoitem.IsComplete = true;
}
return todoitem;
}
//*/
I'm going to map my post and put request to URL http://site/area/controller,
and I used HttpPost and HttpPut attributes.
But when I post data to server, it returned 404 Not found.
My code:
public class ApplicationsController : Controller
{
private readonly IApplicationService _applicationService;
public ApplicationsController(IApplicationService applicationService)
{
_applicationService = applicationService;
}
[HttpGet]
public async Task<IActionResult> Index(ApplicationQuery query)
{
var permissionNodes = await _applicationService.SelectePagedApplicationsAsync(query);
ViewData["Query"] = query;
return View(permissionNodes.Data);
}
[HttpPost("/Applications")]
public async Task<IActionResult> Create(ApplicationViewModel model)
{
var app = Mapper.Map<Application>(model);
var result = await _applicationService.CreateApplicationAsync(app);
ViewData["ServiceResult"] = result;
return View(nameof(Edit));
}
[HttpPut("/Applications")]
public async Task<IActionResult> Update(ApplicationViewModel model)
{
var app = Mapper.Map<Application>(model);
var result = await _applicationService.CreateApplicationAsync(app);
ViewData["ServiceResult"] = result;
return View(nameof(Edit));
}
[HttpGet]
public async Task<IActionResult> Edit(long? id)
{
var result = await _applicationService.FindApplicationAsync(new ApplicationQuery
{
Id = id
});
var model = Mapper.Map<ApplicationViewModel>(result.Data);
return View(model);
}
What is the way to make MVC map my request to my action?
You appear to be missing token assignments of the routes in question.
Token replacement in route templates ([controller], [action], [area])
For convenience, attribute routes support token replacement by enclosing a token in square-braces ([, ]). The tokens [action], [area], and [controller] will be replaced with the values of the action name, area name, and controller name from the action where the route is defined.
Seeing as you want to isolate those routes to only the POST and PUT endpoints update the routes accordingly
public class ApplicationsController : Controller {
//...code removed for brevity
//Matches POST /{area}/applications
[HttpPost("[area]/[controller]")]
public async Task<IActionResult> Create([FromBody]ApplicationViewModel model) {
//...code removed for brevity
}
//Matches PUT /{area}/applications
[HttpPut("[area]/[controller]")]
public async Task<IActionResult> Update([FromBody]ApplicationViewModel model) {
//...code removed for brevity
}
//...code removed for brevity
}
The above also assumes that the proper configuration has already been done in relation to areas
Reference Routing to Controller Actions