Controllers And Routing

Here the Controller Name should have the suffix (Controller) because there are lots of (.cs) CSharp files in the project. And this convention helps Visual Studio to identify which is the Controller file. So we can’t remove this suffix ‘Controller’ from any Controller name.

Let’s try to understand Routing, Controllers and Actions. So let’s open Visual Studio and create the HelloWorld ‘Empty’ project.

  • Add an Empty Controller
  • And name it as ‘FirstController’

    Controllers

Here, the Controller Name should have a suffix (Controller) because there are lots of (.cs) C# files in the project. And this convention helps Visual Studio to identify which is the Controller file. So we can’t remove this suffix ‘Controller’ from any Controller name.

Now, remove its built-in scaffolded Function and add this function.
  1. public class FirstController : Controller  
  2. {  
  3.     public string Welcome()  
  4.     {  
  5.         return "Hello, <b>World!</b>";  
  6.     }  
  7. }  
Now come  to the RouteConfig.cs file

  1. routes.MapRoute(  
  2.     name: "Default",  
  3.     url: "{controller}/{action}/{id}",  
  4.     defaults: new { controller = "First", action = "Welcome", id = UrlParameter.Optional }  
  5. );  

This routemap tells us how to request any action from the browser. And by default when we run the application, then the browser will execute ‘Home’ controller and its ‘Index’ action. But we’ve created an Empty template project. As we’ve not set a new route for our controller and action so we need to manually request our controller and actions. In the above route, we can see in URL what is the actual route pattern we need to follow.

As we’ve created an empty application, we don’t have any ‘HomeController’ so when we run our application then it will show the error in the browser because by default it is requesting the ‘HomeController’ as we can see in the above RouteMap.

Controllers

But now, if we want to execute our ‘Welcome’ action then we need to manually request the URL.

Controllers

We know that HTML renders on the browser. So we make the “World!” bold on welcome method returning a string that’s why it is shown to us bold.

Let’s change the route map. ‘id’ is actually optional so it is not necessary to provide id here.
  1. routes.MapRoute(  
  2.     name: "Default",  
  3.     url: "{controller}/{action}/{id}",  
  4.     defaults: new { controller = "First", action = "Welcome", id = UrlParameter.Optional }  
  5. );  

Now, if just run the application, then our routemap knows that he needs to route the ‘First’ controller and ‘Welcome’ action by default.

Press Ctrl + F5.

Controllers

And now, it is working without requesting any URL explicitly.

Let’s copy the Welcome function and paste it.

  1. public class FirstController : Controller  
  2. {  
  3.     public string Welcome()  
  4.     {  
  5.         return "Hello, <b>World!</b>";  
  6.     }  
  7.    
  8.     public string Hello(string name)  
  9.     {  
  10.         return "Hello " + name;  
  11.     }  
  12. }  

ASP.NET MVC automatically maps request data to parameter values for action methods. If an action method takes a parameter, MVC framework looks for a parameter of the same name in the request data, if the parameter with that name exists the framework will automatically pass the value of that parameter to the target action. Let me explain there are 2 different ways to pass the data to actions as action parameters.

URL:                             /first/hello/Usama

Query String:                /first/hello?name=Usama

Let’s try to understand both of the scenarios,

As we’ve modified our route map with ‘First’ controller and ‘Welcome’ action but we need to test our hello action. As we know that the ‘id’ is optional in the route map and ‘id’ is just the identifier to identify the action parameter. So, if we change our parameter name from ‘name’ to ‘id’ then it will work for the first case.

  1. namespace HelloWorld.Controllers  
  2. {  
  3.     public class FirstController : Controller  
  4.     {  
  5.         public string Welcome()  
  6.         {  
  7.             return "Hello, <b>World!</b>";  
  8.         }  
  9.    
  10.         public string Hello(string id, string cast = null)  
  11.         {  
  12.             return "Hello " + id + “ “ + cast;  
  13.         }  
  14.     }  
  15. }  

Now let’s run the application.

Controllers

As ‘Welcome’ action is set in our route map so we need to mention the action name here in the route.

Routemap was telling us the parameter name ‘id’ and also telling us the path.

  1. url: "{controller}/{action}/{id}"  

So we request the page with this URL.

http://localhost:65354/first/hello/usama

And it triggers our hello action.

Now let’s implement with Query String. So, first of all, change the action parameter from ‘id’ to ‘name’.

  1. public string Hello(string id, string cast = null)  
  2. {  
  3.     return "Hello " + id + “ “ + cast;  
  4. }  

Now run the application. And if you request the above URL, then our hello action will not be triggered. But if you use the query string,

http://localhost:65354/first/hello?name=usama

then our hello action triggers because, in the route map, we’ve set the ‘id’ as action parameter so we need to manually supply the name of the parameter of action with its value followed by a question mark (?).

As we can see here we’ve 2 parameters in hello action so to make the 2nd parameter active, we need to supply the value to it as well.

http://localhost:65354/first/hello?name=usama&cast=khan

Controllers

So this is how query string works.

But to pass the data in query strings isn’t a good practice so we should avoid it, we just use this approach whenever we really need it. Actually it shows the complete parameter name its value, action name, controller name so a malicious user can easily hack our application.

Here, we feel the need of convention-based routing.

Routing

The complete cycle of a request is

  • First of all the request goes to routes in (RouteConfig.cs)
  • After proper verification from the routes, the request goes to a specific controller action.
  • And then the response comes back in views (browser)

We often use this route.

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

But there are situations where we need to request the url with multiple parameters e.g.

http://localhost:64341/students/search/name/dob

Where we can search the students by their names and their date of birth.

So let’s see how to create the custom route. Let’s make a custom route before the default one in RouteConfig file.

Open the ‘RouteConfig’ file,

  1. public static void RegisterRoutes(RouteCollection routes)  
  2. {  
  3.     routes.IgnoreRoute("{resource}.axd/{*pathInfo}");  
  4.    
  5.     routes.MapRoute(  
  6.         name: "Default",  
  7.         url: "{controller}/{action}/{id}",  
  8.         defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }  
  9.     );  
  10. }  

You’ll see this code. Most developers don’t know the purpose of IgnoreRoute.

Let me explain. Suppose you’re working on a project and a critical problem occurs in your project and you have to revoke access to the specific controller or module in a production environment, then you’ll use the IgnoreRoute i.e.

  1. public static void RegisterRoutes(RouteCollection routes)  
  2. {  
  3.     routes.IgnoreRoute("{resource}.axd/{*pathInfo}");  
  4.     routes.IgnoreRoute("Customers/");  
  5.    
  6.     routes.MapRoute(  
  7.         name: "Default",  
  8.         url: "{controller}/{action}/{id}",  
  9.         defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }  
  10.     );  
  11. }  

Now, we can’t access the Customers controller. Now you’re still curious to know the first IgnoreRoute purpose, actually .axd files don’t physically exist in the project but ASP.NET uses the URLs with .axd extensions with Web Resource files (ScriptResource.axd and WebResource.axd) internally, so ASP.NET MVC prevents us from trying to handle this kind of request. In simple words, this line is used to revoke the access from files, controller request etc.

Remember

Define the routes from more specific to more generic in a flow.

As we can see ‘Default’ route is most general route in our application, if it comes first before all the other routes where you have to apply some kind of constraints (which are limited to some kind of special URL request) then surely this specific route will not handle that request because your general routes executes first and it will handle that request and you’ll not see the results according to your need.

So define the routes first where you have applied some kind of constraint, which is more specific and then slowly come down and define the less specific to the before route and then, in the end, a general route to handle the request which isn’t handled by the above routes.

routes.RouteMap();

It has multiple overloads but we commonly use one which has 3 parameters {name, URL, defaults}

name:         It is for identification purposes. It should be unique or different from other route names.

URL:              It is for the pattern matching

defaults:    It is for some default fixings. Say if you’re requesting the domain in the browser without any controller name and action name then which controller and action will handle that request.

The very common example of this scenario is when we run the application then by default our Home Controller and its Index action executes because they’re fixed in our ‘default’ controller

Why should ‘name’  be unique?

Actually, the thing is, RouteTable is a class which contains the application routes. And we know a table has a primary key so our routeMap is actually a record and ‘name’ parameter is the primary key of the record.

Open the Application_Start() in Global.asax.cs

  1. protected void Application_Start()  
  2. {  
  3.     AreaRegistration.RegisterAllAreas();  
  4.     FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);  
  5.     RouteConfig.RegisterRoutes(RouteTable.Routes);  
  6.     BundleConfig.RegisterBundles(BundleTable.Bundles);  
  7. }  

Here as we can see, we’re passing the RouteTable routes into RouteConfig.RegisterRoutes method. And it is essential that we need to register the routes in Global.asax file as well, otherwise it will not work.

And our RegisterRoutes method has RouteCollection parameter

  1. public static void RegisterRoutes(RouteCollection routes)  
  2. {  
  3.     routes.IgnoreRoute("{resource}.axd/{*pathInfo}");  
  4.    
  5.     routes.MapRoute(  
  6.         name: "Default",  
  7.         url: "{controller}/{action}/{id}",  
  8.         defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }  
  9.     );  
  10. }  

RouteCollection actually provides the collection of route information to use them by the controller actions. In Simple Words, if we conclude this above discussion we know that Global.asax manages the states of the application and when our application starts, Application_Start() triggers and register the RouteTable and our RouteTable has the collection of routes as RouteCollection. And when the request comes in, it matches the pattern from these records of the RouteTable and if it founds the record of route then the request goes to the relevant controller action otherwise 404 error.

 
But don't take it so much so much serious, RouteTable doesn't physically exist but it works logically like a table. So the name attribute of each route map should be unique. 

Now come back to the point here is our default route which teaches us how to define the custom route. So we need the same three parameters to define the custom route. But 'name' and 'url' are necessary and 'defaults' is optional, you can see here MapRoute is the property of RouteCollection and it is clear that we're defining the routes in the collection of routes which maps in the RouteTable.

  1. public static void RegisterRoutes(RouteCollection routes)  
  2. {  
  3.     routes.IgnoreRoute("{resource}.axd/{*pathInfo}");  
  4.    
  5.     routes.MapRoute(  
  6.         "StudentsByNamesAndDOB",  
  7.         "students/search/{name}/{year}",  
  8.         new { controller = "Students", action = "Index" });  
  9.    
  10.    
  11.     routes.MapRoute(  
  12.         name: "Default",  
  13.         url: "{controller}/{action}/{id}",  
  14.         defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }  
  15.     );  
  16. }  

Now let’s modify the index action in Students controller.

  1. public class StudentsController : Controller  
  2. {  
  3.     // GET: Students  
  4.     public ActionResult Index(string name, int year)  
  5.     {  
  6.         var student = new Student  
  7.         {  
  8.             Name = "Usama",  
  9.             DOB = new DateTime(1900, 10, 25)  
  10.         };  
  11.         return View(student);  
  12.     }  
  13. }  

Now run the application and request the url

http://localhost:64341/students/search

Then the results will be,

Controllers

Here I’m not specifying any parameter, and we got a 404 error because our url doesn’t match any of our route patterns. Now provide the parameters to make the pattern complete.

http://localhost:64341/students/search/usama/17

And if you’ve applied the breakpoint on the first line of the action then you can see the request comes in into the action.

Constraints

We can apply the constraints (limitations) to our route like year parameter should be in 4 digit and name should be in 3 letters.

  1. // We use Anonymous objects for defaults and for constraints  
  2.     routes.MapRoute(  
  3.         "StudentsByNamesAndDOB",  
  4.         "students/search/{name}/{year}",  
  5.         new { controller = "Students", action = "Index" },  
  6.          new { name = @"\d{3}", year = @"\d{4}" }  
  7.     );  

The reason I put @ sign before “” because we’re using escape sequence character and if we’re working with escape sequence character then we need to follow 1 technique out of these 2 methods.

  1. new { year = @"\d{4}", month = "\\d{2}" }  

But this second technique is really ugly.

Now if we try to access the controller action without following these constraints then you’ll see resource not found an error. So we need to follow these rules strictly.

http://localhost:64341/students/search/usa/017

Now it will work.

Now let’s say we want to hit the action when the year should be in range. And it should not work in any other case.

  1. routes.MapRoute(  
  2.     "StudentsByNamesAndDOB",  
  3.     "students/search/{name}/{year}",  
  4.     new { controller = "Students", action = "Index" },  
  5.      new { name = @"\d{3}", year = @"2001 | 2002" }  
  6. );  

Then we’ll use this route map.