Sion
Sion

Reputation: 33

ASP.NET REST service routing

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

Upvotes: 1

Views: 71

Answers (1)

Nkosi
Nkosi

Reputation: 247088

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

Upvotes: 2

Related Questions