Search code examples
c#asp.netasp.net-mvcasp.net-core-mvc

ASP.NET Core 8 MVC: Action can't have named route value other than id


Background

I'm fairly new, relatively speaking to .NET Core and MVC Core having previously been used to .NET 4.x and MVC 5, so please bear with me if I'm asking something obvious; I couldn't find an answer by searching any way.

Stuff used:

  • .NET 8
  • MVC Core
  • jQuery

So I have a controller with actions like the following:

[Area("SomeArea")]
public class SomeNameController : Controller
{
    public SomeNameController(..injected stuff...) { }

    [HttpGet]
    public PartialViewResult AddDialog() {
      ...
    }

    [HttpPost]
    public async Task<IActionResult> AddDialog(MyDialogModel dialogModel) {
      ...
    }

    [HttpDelete]
    [Route("DoSomeDeletion/{uniqueIdentifier}")]
    public async Task<JsonResult> DoSomeDeletion(int uniqueIdentifier)
    {
       ...
       return Json(result);
    }
}

In the Startup.cs where stuff gets configured amongst other things I have:

app.UseEndpoints(endpoints =>
{
    endpoints.MapRazorPages();
    endpoints.MapBlazorHub();

    endpoints.MapControllerRoute(
        name: "areas",
        pattern: "{area:exists}/{controller=index}/{action=Index}/{id?}");

    endpoints.MapControllerRoute(
        name: "default",
        pattern: "{controller=index}/{action=Index}/{id?}");

    endpoints.MapFallbackToPage("/_Host");

});

I have the following JS in my page to add a click handler and do an AJAX delete:

$("#MyGrid").on('click', '.js-delete-btn', function (e) {
    e.preventDefault();
    var url = this.href,
        data = $("#GridForm").serializeObject();
    $.ajax({
        method: "DELETE",
        url: url,
        data: data
    })
        .done(function (jqXHR, textStatus) {
            toastr.success("Yay! Deleted successfully", null, {
                timeout: 5000,
                onHidden: function () { window.location.reload(); }
            });
        })
        .fail(function (jqXHR, textStatus, errorThrown) {
            toastr.error("Uh-oh something went wrong", null, { timeout: 5000 });
        });
});

In the above the href used for the AJAX request will be something like /SomeArea/SomeName/DoSomeDeletion/356

The form that gets serialized contains a anti-forgery token from when I was trying the MVC action with [HttpPost]

Problem

If I set a breakpoint in the DoSomeDeletion action it will never get hit. However, if I take the Route attribute off of it then it will get hit.

What I've tried

  • I tried using [HttpPost] instead of [HttpDelete]
  • For both HttpDelete and HttpPost I used the overload that lets you specify a route template same as the Route attribute
  • If I change the URL to make the Id be a querystring, e.g. ?uniqueIdentifier=365, then it will hit the action and the value will be bound to the actions parameter.

Observations

  • Removing the route attribute such that the action gets hit I see that the Request.RouteValues has an {id} value that is set to the value from the URL I did the DELETE AJAX call to.

Question

I haven't used attributes for routing before so maybe I'm missing something, but in MVC 5 in .NET 4.8 I never had these kinds of issues, I could have 1 or more route values come from the URL and be called whatever I wanted.

What is going on? Why can't I use route value names that I want to use?


Solution

  • After rereading the MS docs, prompted by @mason in the comments, the bit that didn't quite stick in my head is the all or nothing usage of attribute routing vs. classic routing.

    To make this work, based on the original question, I had to add attributes to the controller and the action methods as follows:

    [Area("SomeArea")]
    [Route("[area]/[controller]/[action]")]
    public class SomeNameController : Controller
    {
        public SomeNameController(..injected stuff...) { }
    
        [HttpDelete]
        [Route("{uniqueIdentifier}")]
        public async Task<JsonResult> DoSomeDeletion(int uniqueIdentifier)
        {
           ...
           return Json(result);
        }
    }
    

    I could also have just put it all on the action as follows:

    [Area("SomeArea")]
    [Route("[area]/[controller]/[action]")]
    public class SomeNameController : Controller
    {
        public SomeNameController(..injected stuff...) { }
        
        [HttpDelete]
        [Route("[area]/[controller]/[action]/{uniqueIdentifier}")]
        public async Task<JsonResult> DoSomeDeletion(int uniqueIdentifier)
        {
           ...
           return Json(result);
        }
    }
    

    I took the former route because in the real code there are naturally multiple actions and so didn't want to repeat things.