Custom 404 when no route matches
Playing with MVC gets interesting every time.
Q: What if a user mistypes the url path in an MVC application?
A: A 404 gets displayed.
Q: What if I don’t want the 404 to get displayed.. no matter what the user types in the address bar?
A: Huh??
This is what I’ve been playing with. In theory, at some point the controller factory (the DefaultControllerFactory in most cases) gets to know that there’s no controller named ‘blah’ (when the user types ‘http://website.com/blah’) and decides to display a 404 to the user.
Now, at this point, what if one intercepts this process and says, wait a minute, instead of a 404, show this View (I’m talking MVC View)? I’ve heard MVC is extensible, but is this possible?
I started looking at the source code. I began with the DefaultControllerFactory class and hit the CreateController method and here’s what the implementation looks like:
1: public virtual IController CreateController(RequestContext requestContext, string controllerName)
2: {
3: if (requestContext == null)
4: {
5: throw new ArgumentNullException("requestContext");
6: }
7: if (String.IsNullOrEmpty(controllerName))
8: {
9: throw new ArgumentException(MvcResources.Common_NullOrEmpty, "controllerName");
10: }
11: RequestContext = requestContext;
12: Type controllerType = GetControllerType(controllerName);
13: IController controller = GetControllerInstance(controllerType);
14: return controller;
15: }
Initial observation – it’s marked virtual; yay, I can override it.
This method passes the current request context and the controller name taken from the url (‘blah’ in our case). It does some initial checks and in line 12, it tries to find if there IS a controller with that name. Our example comes back with a ‘null’ for the controllerType.
A null gets passed to the GetControllerInstance method in line 13 and this is what that method looks like:
1: protected internal virtual IController GetControllerInstance(Type controllerType) {
2: if (controllerType == null) {
3: throw new HttpException(404,
4: String.Format(
5: CultureInfo.CurrentUICulture,
6: MvcResources.DefaultControllerFactory_NoControllerFound,
7: RequestContext.HttpContext.Request.Path));
8: }
9: ...
There it is coded clearly in plain English:
- if controllerType is null, throw a 404
(well, not literally, but you get the point).
Here’s one way to get out of this:
- Create an MVC project and add a class ‘BlahControllerFactory.cs’ (cleverly named huh?) to your project
- Derive the class from ‘DefaultControllerFactory’
- Modify the Application_Start (in Global.asax.cs) to the following to make sure the ‘BlahControllerFactory’ gets called and not the DefaultControllerFactory - line 4 does the trick
1: protected void Application_Start()
2: {
3: RegisterRoutes(RouteTable.Routes);
4: ControllerBuilder.Current.SetControllerFactory(new BlahControllerFactory());
5: }
- Override the CreateControllerFactory method to something like below:
1: public override IController CreateController(RequestContext requestContext, string controllerName)
2: {
3: if (requestContext == null)
4: {
5: throw new ArgumentNullException("requestContext");
6: }
7: if (String.IsNullOrEmpty(controllerName))
8: {
9: throw new ArgumentException(MvcResources.Common_NullOrEmpty, "controllerName");
10: }
11: RequestContext = requestContext;
12: Type controllerType = GetControllerType(controllerName);
13: if (controllerType == null)
14: {
15: controllerName = "Home";
16: controllerType = GetControllerType(controllerName);
17: requestContext.RouteData.Values["Controller"] = "Home";
18: requestContext.RouteData.Values["action"] = "Index";
19: }
20: IController controller = GetControllerInstance(controllerType);
21: return controller;
22: }
Let’s look at lines 13-19 (new additions).
- If the controllerType is null, change the controllerName from ‘blah’ to ‘whatever you want’
- Try finding the controllerType again (this should not fail, unless you intentionally want it to)
- Set the values for the RouteData.Values dictionary object
and you’re done.
This is how you can decide to call a Custom404Controller or go back to the Home page as I’ve done above. There’s no need to hardcode the values in here, you can read them from your web.config as well. Just make sure you don’t call the keys in the web.config file as ‘DefaultController’, ‘DefaultAction’, since there’s already a default controller and a default action in your global.asax.cs file. Also, this is not a ‘default’ per say, this is something of the order of an ERROR. So I prefer to call them as ‘RedmondWeHaveAProblemControllerName’ and ‘RedmondWeHaveAProblemAction’.
Have fun.