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;
}
//*/
Related
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
How can I get record by Id? I have the following code to create master detail pages in asp.net core controller, I am able to get all products using the following code and works perfect
Interface
public interface IProductService { Task<IList<ProductDTO>> GetProduct(); }
Controller Actions
public IActionResult Index()
{
return View();
}
[HttpGet]
public async Task<IActionResult> GetProducts()
{
var products = await ProductsService.GetProducts();
return Json(products);
}
But how can I get single record by Id to create a detail page.I tried this but doesn’t work
public IActionResult Detail()
{
return View();
}
[HttpGet]
public async Task<IActionResult> GetProductsDetail(int id)
{
var products = await ProductsService.GetProducts.find(id);
return Json(products);
}
GetProductCode
public class GetProducts_Action : BaseEFAction<GetProducts_Action_Request, GetProducts_Action_Response>
{
public IFileProvider FileProvider { get; }
public GetProducts_Action(ILogger<GetProducts_Action> logger, DBContext context, ITransactionManager scope, IFileProvider fileProvider) : base(logger, context, scope)
{
FileProvider = fileProvider;
}
protected override Task<GetProducts_Action_Response> PerformActionAsync(GetProducts_Action_Request request)
{
IList<ProductDTO> product;
using (var file = System.IO.File.OpenText(FileProvider.GetFileInfo("Product.json").PhysicalPath))
{
var serializer = new JsonSerializer();
product = (IList<ProductDTO>)serializer.Deserialize(file, typeof(IList<ProductDTO>));
}
return Task.FromResult(new GetProducts_Action_Response { Products = product });
}
}
public class GetProducts_Action_Request : BaseActionRequest
{
}
public class GetProducts_Action_Response : BaseActionResponse
{
public IList<ProductDTO> Products { get; set; }
}
}
Given that your data source is actually a file and not a database, you're going to be deserializing that file each time anyway. That is your performance bottleneck. So if you want you can just use your existing GetProducts() service method and then filter in the controller (using LINQ). It's not a super clean way (code-wise), but the performance will basically be the same.
[HttpGet]
public async Task<IActionResult> GetProductsDetail(int id)
{
// Get all the products and then filter by id
// change "a.Id" to the actual DTO Id property name if different
var product = (await ProductsService.GetProducts()).FirstOrDefault(a => a.Id == id);
if (product != null) {
// If we found something, return that single ProductDTO
return Json(product);
} else {
// Not Found or whatever you want
return NotFound();
}
}
FirstOrDefault() will return the first object with your desired ID (assuming the ProductDTO property is called Id). If it doesn't find anything, it will return null, so then you probably want to return a 404 Not Found or something similar.
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));
}
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 created a simple Web API with ASP.NET Core. I have the following API:
GET /api/messages - get all messages
GET /api/messages/{id} - get a message by id
POST /api/messages - add a new message
PUT /api/messages/{id} - update an existing message
DELETE /api/messages/{id} - delete a message
Now, I want another API to get all messages by message owner's name.
What I tried:
I tried to create this API, but it doesn't work because it conflicts with GET /api/messages/{id}:
GET /api/messages/{name} <- (doesn't work due to conflicting API)
// GET: api/messages/{name}
[HttpGet("{name}")]
public IEnumerable<Message> GetMessagesByName(string name)
{
return _repository.GetMessages().Where(m => m.Owner == name);
}
Here is my Message model Message.cs:
public class Message
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; set; }
public string Owner { get; set; }
public string Text { get; set; }
}
Here is my Messages controller MessagesController.cs:
[Route("api/[controller]")]
public class MessagesController : Controller
{
private readonly IMessageRepository _repository;
public MessagesController(IMessageRepository repository)
{
_repository = repository;
}
// GET: api/messages
[HttpGet]
public IEnumerable<Message> Get()
{
return _repository.GetMessages();
}
// GET api/messages/{id}
[HttpGet("{id}", Name = "GetMessage")]
public IActionResult GetById(long id)
{
var message = _repository.GetMessage(id);
if (message == null)
{
return NotFound();
}
return new ObjectResult(message);
}
// POST api/messages
[HttpPost]
public IActionResult Post([FromBody]Message message)
{
if (message == null)
{
return BadRequest();
}
_repository.AddMessage(message);
return CreatedAtRoute("GetMessage", new { id = message.Id }, message);
}
// PUT api/messages/{id}
[HttpPut("{id}")]
public IActionResult Put(long id, [FromBody]Message message)
{
if (message == null || message.Id != id)
{
return BadRequest();
}
var messageToUpdate = _repository.GetMessage(id);
if (messageToUpdate == null)
{
return NotFound();
}
messageToUpdate.Owner = message.Owner;
messageToUpdate.Text = message.Text;
_repository.UpdateMessage(messageToUpdate);
return new NoContentResult();
}
// DELETE api/messages/{id}
[HttpDelete("{id}")]
public IActionResult Delete(long id)
{
var message = _repository.GetMessage(id);
if (message == null)
{
return NotFound();
}
_repository.RemoveMessage(id);
return new NoContentResult();
}
}
Question:
How can I create an API method to get all messages by message owner's name?
Ideally, I would like the API to look like GET /api/messages/{name}, but don't think its possible since it conflicts with GET /api/messages/{id}.
I'm thinking of creating the API like this, but I'm not sure how.
GET /api/messages/name/{name} <- (or something along that line)
Solution:
To have GET /api/messages/{name} working without conflicting with GET /api/messages/{id}, change attribute [HttpGet("{id}", Name="GetMessage")] to [HttpGet("{id:long}", Name="GetMessage")] for public IActionResult GetById(long id) method.
To also have GET /api/messages/name/{name} working, add [Route("name/{name}")] attribute to public IEnumerable<Message> GetMessagesByName(string name) method.
you can put parameter type in route, so your code method should be look like that:
// GET api/messages/{id}
[HttpGet("{id:long}", Name = "GetMessage")]
public IActionResult GetById(long id)
{
var message = _repository.GetMessage(id);
if (message == null)
{
return NotFound();
}
return new ObjectResult(message);
}
I think, web api is ignoring parameters types in routes if they are not typed explicitly, so in your example it has two routes like this: api/messages/{object} and when you put explicit type, they are like this: api/messages/{object} and api/messages/{long}