Search code examples
c#asp.net-mvcasp.net-mvc-routing

Custom route doesn't work with default mvc route


I'm following the Microsoft Contoso University Tutorial to learn MVC.

So far I've created 3 Models

  1. Course
  2. Enrollment
  3. Student

And I have 2 Controllers

  1. HomeController
  2. StudentController

I can view a list of students, details about the students including what courses and what grades they have in the courses, edit student details, and add new students.

At this point, I wanted to learn more about routing since I had been using the default Map Route

routes.MapRoute(
    name: "Default",
    url: "{controller}/{action}/{id}",
    defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);

I have a database of students that all have unique last names. I'm aware that doing this is not normally recommended since last names are not unique, but it works for this small learning project. So what I wanted to achieve was to be able to type in /students/{action}/{lastname} and view details about the student. so I created a new action method in my student controller.

public ActionResult Grab(string studentName)
{
    if (studentName == null)
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);

    var student = db.Students.FirstOrDefault(x => x.LastName.ToLower() == studentName.ToLower());
    if (student == null)
        return HttpNotFound();

    return View(student);
}

And I added a new route in my RouteConfig.cs file

//Custom route
routes.MapRoute(
    name: null,
    url: "{controller}/{action}/{studentName}",
    defaults: new { controller = "Student", action = "Grab", id = UrlParameter.Optional }
);

And finally I created a new view called Grab that shows details about the student.

Now when I type in /student/grab/Norman it gives me the details of the student with the last name Norman

Laura Norman

This is great. But now I have a problem. When I try to use some of my original URLs like /Student/Details/1 they no longer work. I get a 400 error.

The first thing I did was move my default route above the custom route I made

routes.MapRoute(
    name: "Default",
    url: "{controller}/{action}/{id}",
    defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
//Custom route
routes.MapRoute(
    name: null,
    url: "{controller}/{action}/{studentName}",
    defaults: new { controller = "Student", action = "Grab", id = UrlParameter.Optional }
);

And this fixes that problem but causes a 400 error on my previously working grab route. How can I use both of these at the same time without getting a 400 error?

Here is my full RouteConfig.cs file

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace ContosoUniversity
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
            );
            //Custom route
            routes.MapRoute(
                name: null,
                url: "{controller}/{action}/{studentName}",
                defaults: new { controller = "Student", action = "Grab", id = UrlParameter.Optional }
            );


        }
    }
}

Here is my entire StudentController.cs file

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Net;
using System.Web;
using System.Web.Mvc;
using ContosoUniversity.DAL;
using ContosoUniversity.Models;

namespace ContosoUniversity.Controllers
{
    public class StudentController : Controller
    {
        private SchoolContext db = new SchoolContext();

        // GET: Student
        public ActionResult Index()
        {
            return View(db.Students.ToList());
        }

        // GET: Student/Details/5
        public ActionResult Details(int? id)
        {
            if (id == null)
            {
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
            }
            Student student = db.Students.Find(id);
            if (student == null)
            {
                return HttpNotFound();
            }
            return View(student);
        }

        public ActionResult Grab(string studentName)
        {
            if (studentName == null)
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);

            var student = db.Students.FirstOrDefault(x => x.LastName.ToLower() == studentName.ToLower());
            if (student == null)
                return HttpNotFound();

            return View(student);
        }

        // GET: Student/Create
        public ActionResult Create()
        {
            return View();
        }

        // POST: Student/Create
        // To protect from overposting attacks, please enable the specific properties you want to bind to, for 
        // more details see http://go.microsoft.com/fwlink/?LinkId=317598.
        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Create([Bind(Include = "LastName, FirstMidName, EnrollmentDate")]Student student)
        {
            try
            {
                if (ModelState.IsValid)
                {
                    db.Students.Add(student);
                    db.SaveChanges();
                    return RedirectToAction("Index");
                }
            }
            catch (DataException /* dex */)
            {
                //Log the error (uncomment dex variable name and add a line here to write a log.
                ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists see your system administrator.");
            }
            return View(student);
        }

        // GET: Student/Edit/5
        public ActionResult Edit(int? id)
        {
            if (id == null)
            {
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
            }
            Student student = db.Students.Find(id);
            if (student == null)
            {
                return HttpNotFound();
            }
            return View(student);
        }

        // POST: Student/Edit/5
        // To protect from overposting attacks, please enable the specific properties you want to bind to, for 
        // more details see http://go.microsoft.com/fwlink/?LinkId=317598.
        [HttpPost, ActionName("Edit")]
        [ValidateAntiForgeryToken]
        public ActionResult EditPost(int? id)
        {
            if (id == null)
            {
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
            }
            var studentToUpdate = db.Students.Find(id);
            if (TryUpdateModel(studentToUpdate, "",
               new string[] { "LastName", "FirstMidName", "EnrollmentDate" }))
            {
                try
                {
                    db.SaveChanges();

                    return RedirectToAction("Index");
                }
                catch (DataException /* dex */)
                {
                    //Log the error (uncomment dex variable name and add a line here to write a log.
                    ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
                }
            }
            return View(studentToUpdate);
        }

        // GET: Student/Delete/5
        public ActionResult Delete(int? id)
        {
            if (id == null)
            {
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
            }
            Student student = db.Students.Find(id);
            if (student == null)
            {
                return HttpNotFound();
            }
            return View(student);
        }

        // POST: Student/Delete/5
        [HttpPost, ActionName("Delete")]
        [ValidateAntiForgeryToken]
        public ActionResult DeleteConfirmed(int id)
        {
            Student student = db.Students.Find(id);
            db.Students.Remove(student);
            db.SaveChanges();
            return RedirectToAction("Index");
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                db.Dispose();
            }
            base.Dispose(disposing);
        }
    }
}

I'll provide any other details needed. Again I want to be able to type in http://localhost:49706/Student/Details/1 or http://localhost:49706/Student/Grab/Alexander and get the same exact details since Alexander is has the studentID of 1.


Solution

  • You want to differentiate your custom route from the default route url

    public static void RegisterRoutes(RouteCollection routes) {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
    
        //Custom route
        routes.MapRoute(
            name: "Students",
            url: "Student/{action}/{id}",
            defaults: new { controller = "Student", action = "Grab", id = UrlParameter.Optional }
        );
    
        //General default route
        routes.MapRoute(
            name: "Default",
            url: "{controller}/{action}/{id}",
            defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
        );
    }
    

    Action parameters should also match the route template parameters

    // GET: Student/Details/5
    public ActionResult Details(int? id) {
        if (id == null) {
            return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
        }
        var student = db.Students.Find(id);
        if (student == null) {
            return HttpNotFound();
        }
        return View(student);
    }
    
    public ActionResult Grab(string id) {
        if (id == null)
            return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    
        var student = db.Students.FirstOrDefault(x => x.LastName.ToLower() == id.ToLower());
        if (student == null)
            return HttpNotFound();
    
        return View(student);
    }
    

    This would now allow the following to be match correctly

    Student/Details/1 
    Student/Grab/Alexander
    

    provided student with last name Alexander has the studentId of 1